TestByteBasedSymbols.java

package com.fasterxml.jackson.core.sym;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Random;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.*;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Unit test(s) to verify that handling of (byte-based) symbol tables
 * is working.
 */
class TestByteBasedSymbols
        extends com.fasterxml.jackson.core.JUnit5TestBase
{
    final static String[] FIELD_NAMES = new String[] {
        "a", "b", "c", "x", "y", "b13", "abcdefg", "a123",
        "a0", "b0", "c0", "d0", "e0", "f0", "g0", "h0",
        "x2", "aa", "ba", "ab", "b31", "___x", "aX", "xxx",
        "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2",
        "a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3",
        "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1",
    };

    /**
     * This unit test checks that [JACKSON-5] is fixed; if not, a
     * symbol table corruption should result in odd problems.
     */
    @Test
    void sharedSymbols() throws Exception
    {
        // MUST share a single json factory
        JsonFactory jf = new JsonFactory();

        // First things first: parse a dummy doc to populate
        // shared symbol table with some stuff
        String DOC0 = "{ \"a\" : 1, \"x\" : [ ] }";
        JsonParser jp0 = createParser(jf, DOC0);

        /* Important: don't close, don't traverse past end.
         * This is needed to create partial still-in-use symbol
         * table...
         */
        while (jp0.nextToken() != JsonToken.START_ARRAY) { }

        String doc1 = createDoc(FIELD_NAMES, true);
        String doc2 = createDoc(FIELD_NAMES, false);

        // Let's run it twice... shouldn't matter
        for (int x = 0; x < 2; ++x) {
            JsonParser jp1 = createParser(jf, doc1);
            JsonParser jp2 = createParser(jf, doc2);

            assertToken(JsonToken.START_OBJECT, jp1.nextToken());
            assertToken(JsonToken.START_OBJECT, jp2.nextToken());

            int len = FIELD_NAMES.length;
            for (int i = 0; i < len; ++i) {
                assertToken(JsonToken.FIELD_NAME, jp1.nextToken());
                assertToken(JsonToken.FIELD_NAME, jp2.nextToken());
                assertEquals(FIELD_NAMES[i], jp1.currentName());
                assertEquals(FIELD_NAMES[len-(i+1)], jp2.currentName());
                assertToken(JsonToken.VALUE_NUMBER_INT, jp1.nextToken());
                assertToken(JsonToken.VALUE_NUMBER_INT, jp2.nextToken());
                assertEquals(i, jp1.getIntValue());
                assertEquals(i, jp2.getIntValue());
            }

            assertToken(JsonToken.END_OBJECT, jp1.nextToken());
            assertToken(JsonToken.END_OBJECT, jp2.nextToken());

            jp1.close();
            jp2.close();
        }
        jp0.close();
    }

    @Test
    void auxMethodsWithNewSymboTable() throws Exception
    {
        final int A_BYTES = 0x41414141; // "AAAA"
        final int B_BYTES = 0x42424242; // "BBBB"

        ByteQuadsCanonicalizer nc = ByteQuadsCanonicalizer.createRoot()
                .makeChild(JsonFactory.Feature.collectDefaults());
        assertNull(nc.findName(A_BYTES));
        assertNull(nc.findName(A_BYTES, B_BYTES));

        nc.addName("AAAA", new int[] { A_BYTES }, 1);
        String n1 = nc.findName(A_BYTES);
        assertEquals("AAAA", n1);
        nc.addName("AAAABBBB", new int[] { A_BYTES, B_BYTES }, 2);
        String n2 = nc.findName(A_BYTES, B_BYTES);
        assertEquals("AAAABBBB", n2);
        assertNotNull(n2);

        /* and let's then just exercise this method so it gets covered;
         * it's only used for debugging.
         */
        assertNotNull(nc.toString());
    }

    // as per name, for [core#207]
    @Test
    void issue207() throws Exception
    {
        ByteQuadsCanonicalizer nc = ByteQuadsCanonicalizer.createRoot(-523743345);
        Field byteSymbolCanonicalizerField = JsonFactory.class.getDeclaredField("_byteSymbolCanonicalizer");
        byteSymbolCanonicalizerField.setAccessible(true);
        JsonFactory jsonF = new JsonFactory();
        byteSymbolCanonicalizerField.set(jsonF, nc);

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{\n");
        stringBuilder.append("    \"expectedGCperPosition\": null");
        for (int i = 0; i < 60; ++i) {
            stringBuilder.append(",\n    \"").append(i + 1).append("\": null");
        }
        stringBuilder.append("\n}");

        JsonParser p = jsonF.createParser(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
        while (p.nextToken() != null) { }
        p.close();
    }

    // [core#548]
    @Test
    void quadsIssue548() throws Exception
    {
        Random r = new Random(42);
        ByteQuadsCanonicalizer root = ByteQuadsCanonicalizer.createRoot();
        ByteQuadsCanonicalizer canon = root.makeChild(JsonFactory.Feature.collectDefaults());

        int n_collisions = 25;
        int[] collisions = new int[n_collisions];

        // generate collisions
        {
            int maybe = r.nextInt();
            int hash = canon.calcHash(maybe);
            int target = ((hash & (2048-1)) << 2);

            for (int i = 0; i < collisions.length; ) {
                maybe = r.nextInt();
                hash = canon.calcHash(maybe);
                int offset = ((hash & (2048-1)) << 2);

                if (offset == target) {
                    collisions[i++] = maybe;
                }
            }
        }

        // fill spillover area until _needRehash is true.
        for(int i = 0; i < 22 ; i++) {
            canon.addName(Integer.toString(i), collisions[i]);
        }
        // canon._needRehash is now true, since the spillover is full

        // release table to update tableinfo with canon's data
        canon.release();

        // new table pulls data from new tableinfo, that has a full spillover, but set _needRehash to false
        canon = root.makeChild(JsonFactory.Feature.collectDefaults());

        // canon._needRehash == false, so this will try to add another item to the spillover area, even though it is full
        canon.addName(Integer.toString(22), collisions[22]);
    }

    /*
    /**********************************************************
    /* Helper methods
    /**********************************************************
     */

    protected JsonParser createParser(JsonFactory jf, String input) throws Exception
    {
        byte[] data = input.getBytes(StandardCharsets.UTF_8);
        InputStream is = new ByteArrayInputStream(data);
        return jf.createParser(is);
    }

    private String createDoc(String[] fieldNames, boolean add)
    {
        StringBuilder sb = new StringBuilder();
        sb.append("{ ");

        int len = fieldNames.length;
        for (int i = 0; i < len; ++i) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append('"');
            sb.append(add ? fieldNames[i] : fieldNames[len - (i+1)]);
            sb.append("\" : ");
            sb.append(i);
        }
        sb.append(" }");
        return sb.toString();
    }
}