/* * Copyright 2016 MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * */ package org.bson; import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.io.BasicOutputBuffer; import org.bson.json.JsonMode; import org.bson.json.JsonWriterSettings; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import util.Hex; import util.JsonPoweredTestHelper; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import static java.lang.String.format; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; // BSON tests powered by language-agnostic JSON-based tests included in test resources @RunWith(Parameterized.class) public class GenericBsonTest { enum TestCaseType { VALID, PARSE_ERROR } private final Set<String> testsToSkip = new HashSet<String>(Arrays.asList( "DBpointer with opposite key order", // JsonReader does not support out of order keys "DBpointer with extra keys" // JsonReader does not support extra keys )); private final BsonDocument testDefinition; private final BsonDocument testCase; private final TestCaseType testCaseType; public GenericBsonTest(final String description, final BsonDocument testDefinition, final BsonDocument testCase, final TestCaseType testCaseType) { this.testDefinition = testDefinition; this.testCase = testCase; this.testCaseType = testCaseType; } @Test public void shouldPassAllOutcomes() { assumeTrue(!testsToSkip.contains(testCase.getString("description").getValue())); switch (testCaseType) { case VALID: runValid(); break; case PARSE_ERROR: runDecodeError(); break; default: throw new IllegalArgumentException(format("Unsupported test case type %s", testCaseType)); } } private void runValid() { String bsonHex = testCase.getString("bson").getValue().toUpperCase(); String json = replaceUnicodeEscapes(testCase.getString("extjson", new BsonString("")).getValue()); String canonicalJson = replaceUnicodeEscapes(testCase.getString("canonical_extjson", new BsonString(json)).getValue()); String canonicalBsonHex = testCase.getString("canonical_bson", new BsonString(bsonHex)).getValue().toUpperCase(); String description = testCase.getString("description").getValue(); boolean lossy = testCase.getBoolean("lossy", new BsonBoolean(false)).getValue(); BsonDocument decodedDocument = decodeToDocument(bsonHex, description); // B -> B assertEquals(format("Failed to create expected BSON for document with description '%s'", description), canonicalBsonHex, encodeToHex(decodedDocument)); JsonWriterSettings jsonWriterSettings = JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build(); // B -> E if (!canonicalJson.isEmpty()) { assertEquals(format("Failed to create expected JSON for document with description '%s'", description), stripWhiteSpace(canonicalJson), stripWhiteSpace(decodedDocument.toJson(jsonWriterSettings))); } if (!canonicalBsonHex.equals(bsonHex)) { BsonDocument decodedCanonicalDocument = decodeToDocument(canonicalBsonHex, description); // B -> B assertEquals(format("Failed to create expected BSON for canonical document with description '%s'", description), canonicalBsonHex, encodeToHex(decodedCanonicalDocument)); // B -> E assertEquals(format("Failed to create expected JSON for canonical document with description '%s'", description), stripWhiteSpace(canonicalJson), stripWhiteSpace(decodedCanonicalDocument.toJson(jsonWriterSettings))); } if (!json.isEmpty()) { BsonDocument parsedDocument = BsonDocument.parse(json); // E -> E assertEquals(format("Failed to parse expected JSON for document with description '%s'", description), stripWhiteSpace(canonicalJson), stripWhiteSpace(parsedDocument.toJson(jsonWriterSettings))); if (!lossy) { // E -> B assertEquals(format("Failed to create expected BsonDocument for parsed canonical JSON document with description '%s'", description), decodedDocument, parsedDocument); assertEquals(format("Failed to create expected BSON for parsed JSON document with description '%s'", description), canonicalBsonHex, encodeToHex(parsedDocument)); } if (!canonicalJson.equals(json)) { BsonDocument parsedCanonicalDocument = BsonDocument.parse(canonicalJson); // E -> E assertEquals(format("Failed to create expected JSON for parsed canonical JSON document with description '%s'", description), stripWhiteSpace(canonicalJson), stripWhiteSpace(parsedCanonicalDocument.toJson(jsonWriterSettings))); if (!lossy) { // E -> B assertEquals(format("Failed to create expected BsonDocument for parsed canonical JSON document " + "with description '%s'", description), decodedDocument, parsedCanonicalDocument); assertEquals(format("Failed to create expected BSON for parsed canonical JSON document with description '%s'", description), bsonHex, encodeToHex(parsedDocument)); } } } } // The corpus escapes all non-ascii characters, but JSONWriter does not. This method converts the Unicode escape sequence into its // regular UTF encoding in order to match the JSONWriter behavior. private String replaceUnicodeEscapes(final String json) { try { StringReader reader = new StringReader(json); StringWriter writer = new StringWriter(); int cur; while ((cur = reader.read()) != -1) { char curChar = (char) cur; if (curChar != '\\') { writer.write(curChar); continue; } char nextChar = (char) reader.read(); if (nextChar != 'u') { writer.write(curChar); writer.write(nextChar); continue; } char[] codePointString = new char[4]; reader.read(codePointString); char escapedChar = (char) Integer.parseInt(new String(codePointString), 16); if (shouldEscapeCharacter(escapedChar)) { writer.write("\\u0000"); } else { writer.write(escapedChar); } } return writer.toString(); } catch (IOException e) { throw new RuntimeException("impossible"); } } // copied from JsonWriter... private boolean shouldEscapeCharacter(final char escapedChar) { switch (Character.getType(escapedChar)) { case Character.UPPERCASE_LETTER: case Character.LOWERCASE_LETTER: case Character.TITLECASE_LETTER: case Character.OTHER_LETTER: case Character.DECIMAL_DIGIT_NUMBER: case Character.LETTER_NUMBER: case Character.OTHER_NUMBER: case Character.SPACE_SEPARATOR: case Character.CONNECTOR_PUNCTUATION: case Character.DASH_PUNCTUATION: case Character.START_PUNCTUATION: case Character.END_PUNCTUATION: case Character.INITIAL_QUOTE_PUNCTUATION: case Character.FINAL_QUOTE_PUNCTUATION: case Character.OTHER_PUNCTUATION: case Character.MATH_SYMBOL: case Character.CURRENCY_SYMBOL: case Character.MODIFIER_SYMBOL: case Character.OTHER_SYMBOL: return false; default: return true; } } private BsonDocument decodeToDocument(final String subjectHex, final String description) { ByteBuffer byteBuffer = ByteBuffer.wrap(Hex.decode(subjectHex)); BsonDocument actualDecodedDocument = new BsonDocumentCodec().decode(new BsonBinaryReader(byteBuffer), DecoderContext.builder().build()); if (byteBuffer.hasRemaining()) { throw new BsonSerializationException(format("Should have consumed all bytes, but " + byteBuffer.remaining() + " still remain in the buffer for document with description ", description)); } return actualDecodedDocument; } private String encodeToHex(final BsonDocument decodedDocument) { BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); new BsonDocumentCodec().encode(new BsonBinaryWriter(outputBuffer), decodedDocument, EncoderContext.builder().build()); return Hex.encode(outputBuffer.toByteArray()); } private void runDecodeError() { try { String description = testCase.getString("description").getValue(); throwIfValueIsStringContainingReplacementCharacter(description); fail(format("Should have failed parsing for subject with description '%s'", description)); } catch (BsonSerializationException e) { // all good } } // TODO: Working around the fact that the Java driver doesn't report an error for invalid UTF-8, but rather replaces the invalid // sequence with the replacement character private void throwIfValueIsStringContainingReplacementCharacter(final String description) { BsonDocument decodedDocument = decodeToDocument(testCase.getString("bson").getValue(), description); String testKey = decodedDocument.keySet().iterator().next(); if (decodedDocument.containsKey(testKey)) { String decodedString = null; if (decodedDocument.get(testKey).isString()) { decodedString = decodedDocument.getString(testKey).getValue(); } if (decodedDocument.get(testKey).isDBPointer()) { decodedString = decodedDocument.get(testKey).asDBPointer().getNamespace(); } if (decodedString != null && decodedString.contains(Charset.forName("UTF-8").newDecoder().replacement())) { throw new BsonSerializationException("String contains replacement character"); } } } @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() throws URISyntaxException, IOException { List<Object[]> data = new ArrayList<Object[]>(); for (File file : JsonPoweredTestHelper.getTestFiles("/bson")) { BsonDocument testDocument = JsonPoweredTestHelper.getTestDocument(file); for (BsonValue curValue : testDocument.getArray("valid", new BsonArray())) { BsonDocument testCaseDocument = curValue.asDocument(); data.add(new Object[]{createTestCaseDescription(testDocument, testCaseDocument, "valid"), testDocument, testCaseDocument, TestCaseType.VALID}); } for (BsonValue curValue : testDocument.getArray("decodeErrors", new BsonArray())) { BsonDocument testCaseDocument = curValue.asDocument(); data.add(new Object[]{createTestCaseDescription(testDocument, testCaseDocument, "parseError"), testDocument, testCaseDocument, TestCaseType.PARSE_ERROR}); } } return data; } private static String createTestCaseDescription(final BsonDocument testDocument, final BsonDocument testCaseDocument, final String testCaseType) { return testDocument.getString("description").getValue() + "[" + testCaseType + "]" + ": " + testCaseDocument.getString("description").getValue(); } private String stripWhiteSpace(final String json) { return json.replace(" ", ""); } }