diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java index 8e47a469d..4a3e97e55 100644 --- a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java @@ -19,8 +19,7 @@ import com.fasterxml.jackson.core.json.JsonWriteContext; import com.fasterxml.jackson.core.io.IOContext; -public class YAMLGenerator extends GeneratorBase -{ +public class YAMLGenerator extends GeneratorBase { /** * Enumeration that defines all togglable features for YAML generators */ @@ -62,7 +61,7 @@ public enum Feature implements FormatFeature // since 2.9 /** * Options passed to SnakeYAML that determines whether longer textual content * gets automatically split into multiple lines or not. - *

+ *

* Feature is enabled by default to conform to SnakeYAML defaults as well as * backwards compatibility with 2.5 and earlier versions. * @@ -73,7 +72,7 @@ public enum Feature implements FormatFeature // since 2.9 /** * Whether strings will be rendered without quotes (true) or * with quotes (false, default). - *

+ *

* Minimized quote usage makes for more human readable output; however, content is * limited to printable characters according to the rules of * literal block style. @@ -85,7 +84,7 @@ public enum Feature implements FormatFeature // since 2.9 /** * Whether numbers stored as strings will be rendered with quotes (true) or * without quotes (false, default) when MINIMIZE_QUOTES is enabled. - *

+ *

* Minimized quote usage makes for more human readable output; however, content is * limited to printable characters according to the rules of * literal block style. @@ -108,7 +107,7 @@ public enum Feature implements FormatFeature // since 2.9 /** * Feature enabling of which adds indentation for array entry generation * (default indentation being 2 spaces). - *

+ *

* Default value is `false` for backwards compatibility * * @since 2.9 @@ -134,8 +133,7 @@ public enum Feature implements FormatFeature // since 2.9 * Method that calculates bit set (flags) of all features that * are enabled by default. */ - public static int collectDefaults() - { + public static int collectDefaults() { int flags = 0; for (Feature f : values()) { if (f.enabledByDefault()) { @@ -151,11 +149,19 @@ private Feature(boolean defaultState) { } @Override - public boolean enabledByDefault() { return _defaultState; } + public boolean enabledByDefault() { + return _defaultState; + } + @Override - public boolean enabledIn(int flags) { return (flags & _mask) != 0; } + public boolean enabledIn(int flags) { + return (flags & _mask) != 0; + } + @Override - public int getMask() { return _mask; } + public int getMask() { + return _mask; + } } /* @@ -231,10 +237,9 @@ private Feature(boolean defaultState) { */ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures, - ObjectCodec codec, Writer out, - org.yaml.snakeyaml.DumperOptions.Version version) - throws IOException - { + ObjectCodec codec, Writer out, + org.yaml.snakeyaml.DumperOptions.Version version) + throws IOException { super(jsonFeatures, codec); _ioContext = ctxt; _formatFeatures = yamlFeatures; @@ -245,7 +250,7 @@ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures, _emitter = new Emitter(_writer, _outputOptions); // should we start output now, or try to defer? _emitter.emit(new StreamStartEvent(null, null)); - Map noTags = Collections.emptyMap(); + Map noTags = Collections.emptyMap(); boolean startMarker = Feature.WRITE_DOC_START_MARKER.enabledIn(yamlFeatures); @@ -255,8 +260,7 @@ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures, } protected DumperOptions buildDumperOptions(int jsonFeatures, int yamlFeatures, - org.yaml.snakeyaml.DumperOptions.Version version) - { + org.yaml.snakeyaml.DumperOptions.Version version) { DumperOptions opt = new DumperOptions(); // would we want canonical? if (Feature.CANONICAL_OUTPUT.enabledIn(_formatFeatures)) { @@ -305,8 +309,7 @@ public Version version() { * Not sure what to do here; could reset indentation to some value maybe? */ @Override - public YAMLGenerator useDefaultPrettyPrinter() - { + public YAMLGenerator useDefaultPrettyPrinter() { return this; } @@ -352,7 +355,9 @@ public boolean canUseSchema(FormatSchema schema) { } @Override - public boolean canWriteFormattedNumbers() { return true; } + public boolean canWriteFormattedNumbers() { + return true; + } //@Override public void setSchema(FormatSchema schema) @@ -396,8 +401,7 @@ public YAMLGenerator configure(Feature f, boolean state) { */ @Override - public final void writeFieldName(String name) throws IOException - { + public final void writeFieldName(String name) throws IOException { if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } @@ -406,8 +410,7 @@ public final void writeFieldName(String name) throws IOException @Override public final void writeFieldName(SerializableString name) - throws IOException - { + throws IOException { // Object is a value, need to verify it's allowed if (_writeContext.writeFieldName(name.getValue()) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); @@ -417,8 +420,7 @@ public final void writeFieldName(SerializableString name) @Override public final void writeStringField(String fieldName, String value) - throws IOException - { + throws IOException { if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } @@ -427,8 +429,7 @@ public final void writeStringField(String fieldName, String value) } private final void _writeFieldName(String name) - throws IOException - { + throws IOException { _writeScalar(name, "string", STYLE_NAME); } @@ -439,14 +440,12 @@ private final void _writeFieldName(String name) */ @Override - public final void flush() throws IOException - { + public final void flush() throws IOException { _writer.flush(); } @Override - public void close() throws IOException - { + public void close() throws IOException { if (!isClosed()) { _emitter.emit(new DocumentEndEvent(null, null, false)); _emitter.emit(new StreamEndEvent(null, null)); @@ -462,8 +461,7 @@ public void close() throws IOException */ @Override - public final void writeStartArray() throws IOException - { + public final void writeStartArray() throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(); Boolean style = _outputOptions.getDefaultFlowStyle().getStyleBoolean(); @@ -474,14 +472,13 @@ public final void writeStartArray() throws IOException _objectId = null; } _emitter.emit(new SequenceStartEvent(anchor, yamlTag, - implicit, null, null, style)); + implicit, null, null, style)); } @Override - public final void writeEndArray() throws IOException - { + public final void writeEndArray() throws IOException { if (!_writeContext.inArray()) { - _reportError("Current context not Array but "+_writeContext.typeDesc()); + _reportError("Current context not Array but " + _writeContext.typeDesc()); } // just to make sure we don't "leak" type ids _typeId = null; @@ -490,8 +487,7 @@ public final void writeEndArray() throws IOException } @Override - public final void writeStartObject() throws IOException - { + public final void writeStartObject() throws IOException { _verifyValueWrite("start an object"); _writeContext = _writeContext.createChildObjectContext(); Boolean style = _outputOptions.getDefaultFlowStyle().getStyleBoolean(); @@ -506,10 +502,9 @@ public final void writeStartObject() throws IOException } @Override - public final void writeEndObject() throws IOException - { + public final void writeEndObject() throws IOException { if (!_writeContext.inObject()) { - _reportError("Current context not Object but "+_writeContext.typeDesc()); + _reportError("Current context not Object but " + _writeContext.typeDesc()); } // just to make sure we don't "leak" type ids _typeId = null; @@ -524,8 +519,7 @@ public final void writeEndObject() throws IOException */ @Override - public void writeString(String text) throws IOException,JsonGenerationException - { + public void writeString(String text) throws IOException, JsonGenerationException { if (text == null) { writeNull(); return; @@ -533,7 +527,7 @@ public void writeString(String text) throws IOException,JsonGenerationException _verifyValueWrite("write String value"); Character style = STYLE_QUOTED; if (Feature.MINIMIZE_QUOTES.enabledIn(_formatFeatures) && !isBooleanContent(text)) { - // If this string could be interpreted as a number, it must be quoted. + // If this string could be interpreted as a number, it must be quoted. if (Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS.enabledIn(_formatFeatures) && PLAIN_NUMBER_P.matcher(text).matches()) { style = STYLE_QUOTED; @@ -553,29 +547,25 @@ private boolean isBooleanContent(String text) { } @Override - public void writeString(char[] text, int offset, int len) throws IOException - { + public void writeString(char[] text, int offset, int len) throws IOException { writeString(new String(text, offset, len)); } @Override public final void writeString(SerializableString sstr) - throws IOException - { + throws IOException { writeString(sstr.toString()); } @Override public void writeRawUTF8String(byte[] text, int offset, int len) - throws IOException - { + throws IOException { _reportUnsupportedOperation(); } @Override public final void writeUTF8String(byte[] text, int offset, int len) - throws IOException - { + throws IOException { writeString(new String(text, offset, len, "UTF-8")); } @@ -627,15 +617,14 @@ public void writeRawValue(char[] text, int offset, int len) throws IOException { */ @Override - public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException - { + public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException { if (data == null) { writeNull(); return; } _verifyValueWrite("write Binary value"); - if (offset > 0 || (offset+len) != data.length) { - data = Arrays.copyOfRange(data, offset, offset+len); + if (offset > 0 || (offset + len) != data.length) { + data = Arrays.copyOfRange(data, offset, offset + len); } _writeScalarBinary(b64variant, data); } @@ -647,22 +636,19 @@ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int l */ @Override - public void writeBoolean(boolean state) throws IOException - { + public void writeBoolean(boolean state) throws IOException { _verifyValueWrite("write boolean value"); _writeScalar(state ? "true" : "false", "bool", STYLE_SCALAR); } @Override - public void writeNumber(int i) throws IOException - { + public void writeNumber(int i) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(i), "int", STYLE_SCALAR); } @Override - public void writeNumber(long l) throws IOException - { + public void writeNumber(long l) throws IOException { // First: maybe 32 bits is enough? if (l <= MAX_INT_AS_LONG && l >= MIN_INT_AS_LONG) { writeNumber((int) l); @@ -673,8 +659,7 @@ public void writeNumber(long l) throws IOException } @Override - public void writeNumber(BigInteger v) throws IOException - { + public void writeNumber(BigInteger v) throws IOException { if (v == null) { writeNull(); return; @@ -684,22 +669,19 @@ public void writeNumber(BigInteger v) throws IOException } @Override - public void writeNumber(double d) throws IOException - { + public void writeNumber(double d) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(d), "double", STYLE_SCALAR); } @Override - public void writeNumber(float f) throws IOException - { + public void writeNumber(float f) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(f), "float", STYLE_SCALAR); } @Override - public void writeNumber(BigDecimal dec) throws IOException - { + public void writeNumber(BigDecimal dec) throws IOException { if (dec == null) { writeNull(); return; @@ -710,8 +692,7 @@ public void writeNumber(BigDecimal dec) throws IOException } @Override - public void writeNumber(String encodedValue) throws IOException,JsonGenerationException, UnsupportedOperationException - { + public void writeNumber(String encodedValue) throws IOException, JsonGenerationException, UnsupportedOperationException { if (encodedValue == null) { writeNull(); return; @@ -721,8 +702,7 @@ public void writeNumber(String encodedValue) throws IOException,JsonGenerationEx } @Override - public void writeNull() throws IOException - { + public void writeNull() throws IOException { _verifyValueWrite("write null value"); // no real type for this, is there? _writeScalar("null", "object", STYLE_SCALAR); @@ -750,16 +730,14 @@ public boolean canWriteTypeId() { @Override public void writeTypeId(Object id) - throws IOException - { + throws IOException { // should we verify there's no preceding type id? _typeId = String.valueOf(id); } @Override public void writeObjectRef(Object id) - throws IOException - { + throws IOException { _verifyValueWrite("write Object reference"); AliasEvent evt = new AliasEvent(String.valueOf(id), null, null); _emitter.emit(evt); @@ -767,8 +745,7 @@ public void writeObjectRef(Object id) @Override public void writeObjectId(Object id) - throws IOException - { + throws IOException { // should we verify there's no preceding id? _objectId = String.valueOf(id); } @@ -781,11 +758,10 @@ public void writeObjectId(Object id) @Override protected final void _verifyValueWrite(String typeMsg) - throws IOException - { + throws IOException { int status = _writeContext.writeValue(); if (status == JsonWriteContext.STATUS_EXPECT_NAME) { - _reportError("Can not "+typeMsg+", expecting field name"); + _reportError("Can not " + typeMsg + ", expecting field name"); } } @@ -806,26 +782,65 @@ protected void _releaseBuffers() { // ... and sometimes we specifically DO want explicit tag: private final static ImplicitTuple EXPLICIT_TAGS = new ImplicitTuple(false, false); - protected void _writeScalar(String value, String type, Character style) throws IOException - { + protected void _writeScalar(String value, String type, Character style) throws IOException { _emitter.emit(_scalarEvent(value, style)); } private void _writeScalarBinary(Base64Variant b64variant, - byte[] data) throws IOException - { + byte[] data) throws IOException { // 15-Dec-2017, tatu: as per [dataformats-text#62], can not use SnakeYAML's internal // codec. Also: force use of linefeed variant if using default if (b64variant == Base64Variants.getDefaultVariant()) { b64variant = Base64Variants.MIME; } - String encoded = b64variant.encode(data); + String encoded = encode(b64variant, data); _emitter.emit(new ScalarEvent(null, TAG_BINARY, EXPLICIT_TAGS, encoded, null, null, STYLE_BASE64)); } - protected ScalarEvent _scalarEvent(String value, Character style) - { + // NOTE: copied and slightly modified from Jackson core Base64Variant.encode(byte[] input, boolean addQuotes) + // the line breaks are printed without escaping and according to the USE_PLATFORM_LINE_BREAKS setting + private String encode(Base64Variant b64variant, byte[] input) { + int inputEnd = input.length; + StringBuilder sb; + { + // let's approximate... 33% overhead, ~= 3/8 (0.375) + int outputLen = inputEnd + (inputEnd >> 2) + (inputEnd >> 3); + sb = new StringBuilder(outputLen); + } + + int chunksBeforeLF = b64variant.getMaxLineLength() >> 2; + + // Ok, first we loop through all full triplets of data: + int inputPtr = 0; + int safeInputEnd = inputEnd-3; // to get only full triplets + + while (inputPtr <= safeInputEnd) { + // First, mash 3 bytes into lsb of 32-bit int + int b24 = ((int) input[inputPtr++]) << 8; + b24 |= ((int) input[inputPtr++]) & 0xFF; + b24 = (b24 << 8) | (((int) input[inputPtr++]) & 0xFF); + b64variant.encodeBase64Chunk(sb, b24); + if (--chunksBeforeLF <= 0) { + sb.append(_outputOptions.getLineBreak().getString()); + chunksBeforeLF = b64variant.getMaxLineLength() >> 2; + } + } + + // And then we may have 1 or 2 leftover bytes to encode + int inputLeft = inputEnd - inputPtr; // 0, 1 or 2 + if (inputLeft > 0) { // yes, but do we have room for output? + int b24 = ((int) input[inputPtr++]) << 16; + if (inputLeft == 2) { + b24 |= (((int) input[inputPtr++]) & 0xFF) << 8; + } + b64variant.encodeBase64Partial(sb, b24, inputLeft); + } + + return sb.toString(); + } + + protected ScalarEvent _scalarEvent(String value, Character style) { String yamlTag = _typeId; if (yamlTag != null) { _typeId = null; diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/BinaryReadTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/BinaryReadTest.java index 0b70b4fd5..1594a7799 100644 --- a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/BinaryReadTest.java +++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/BinaryReadTest.java @@ -1,14 +1,23 @@ package com.fasterxml.jackson.dataformat.yaml.deser; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; +import java.util.Random; import org.junit.Assert; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.dataformat.yaml.ModuleTestBase; +import static org.junit.Assert.assertArrayEquals; + public class BinaryReadTest extends ModuleTestBase { private final ObjectMapper MAPPER = newObjectMapper(); @@ -41,4 +50,28 @@ public void testBinaryViaTree() throws Exception final byte[] expectedFileHeader = new byte[]{'G', 'I', 'F', '8', '9', 'a'}; Assert.assertArrayEquals(expectedFileHeader, actualFileHeader); } + + public void testReadLongBinary() throws Exception { + final byte[] data = new byte[1000]; + new Random().nextBytes(data); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + JsonFactory factory = MAPPER.getFactory(); + JsonGenerator gen = factory.createGenerator(os); + + gen.writeStartObject(); + gen.writeBinaryField("data", data); + gen.writeEndObject(); + gen.close(); + + try (JsonParser parser = factory.createParser(os.toByteArray())) { + assertEquals(JsonToken.START_OBJECT, parser.nextToken()); + assertEquals(JsonToken.FIELD_NAME, parser.nextToken()); + assertEquals("data", parser.currentName()); + assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, parser.nextToken()); + assertArrayEquals(data, parser.getBinaryValue()); + assertEquals(JsonToken.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + } } diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/BinaryWriteTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/BinaryWriteTest.java index da8b86fe7..4d770afb0 100644 --- a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/BinaryWriteTest.java +++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/BinaryWriteTest.java @@ -1,8 +1,13 @@ package com.fasterxml.jackson.dataformat.yaml.ser; +import java.io.StringWriter; +import java.util.Arrays; + import org.junit.Assert; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.ModuleTestBase; @@ -26,4 +31,28 @@ public void testBinaryViaTree() throws Exception final byte[] b = data.binaryValue(); Assert.assertArrayEquals(srcPayload, b); } + + public void testWriteLongBinary() throws Exception { + final int length = 200; + final byte[] data = new byte[length]; + Arrays.fill(data, (byte) 1); + + StringWriter w = new StringWriter(); + JsonGenerator gen = MAPPER.getFactory().createGenerator(w); + + gen.writeStartObject(); + gen.writeBinaryField("array", data); + gen.writeEndObject(); + gen.close(); + + String yaml = w.toString(); + Assert.assertEquals("---\n" + + "array: !!binary |-\n" + + " AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" + + " AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" + + " AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" + + " AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n", yaml); + + } + }