View Javadoc
1   /*
2   Copyright (c) 2008 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.lang.reflect.Field;
20  import java.nio.ByteBuffer;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import com.healthmarketscience.jackcess.ColumnBuilder;
25  import com.healthmarketscience.jackcess.Cursor;
26  import com.healthmarketscience.jackcess.CursorBuilder;
27  import com.healthmarketscience.jackcess.DataType;
28  import com.healthmarketscience.jackcess.Database;
29  import com.healthmarketscience.jackcess.DateTimeType;
30  import com.healthmarketscience.jackcess.Index;
31  import com.healthmarketscience.jackcess.IndexBuilder;
32  import com.healthmarketscience.jackcess.Row;
33  import com.healthmarketscience.jackcess.Table;
34  import com.healthmarketscience.jackcess.TableBuilder;
35  import static com.healthmarketscience.jackcess.TestUtil.*;
36  import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
37  import static org.junit.jupiter.api.Assertions.*;
38  import org.junit.jupiter.api.Test;
39  
40  /**
41   * @author James Ahlborn
42   */
43  public class IndexCodesTest {
44  
45    private static final Map<Character,String> SPECIAL_CHARS =
46      new HashMap<Character,String>();
47    static {
48      SPECIAL_CHARS.put('\b', "\\b");
49      SPECIAL_CHARS.put('\t', "\\t");
50      SPECIAL_CHARS.put('\n', "\\n");
51      SPECIAL_CHARS.put('\f', "\\f");
52      SPECIAL_CHARS.put('\r', "\\r");
53      SPECIAL_CHARS.put('\"', "\\\"");
54      SPECIAL_CHARS.put('\'', "\\'");
55      SPECIAL_CHARS.put('\\', "\\\\");
56    }
57  
58    @Test
59    public void testIndexCodes() throws Exception
60    {
61      doTestDb(Basename.INDEX_CODES);
62    }
63  
64    @Test
65    public void testEmoticons() throws Exception
66    {
67      doTestDb(Basename.EMOTICONS);
68    }
69  
70    /**
71     * Tests the "international ext" characters, whose codes end with a suffix
72     * whose length grows with the number of characters which took that path.
73     * The expected keys were read back out of the index pages Access built for
74     * U+3041 (crazy flag set) and U+3042 (crazy flag clear).
75     */
76    @Test
77    public void testInternationalExtCodes() throws Exception
78    {
79      try(Database db = create(Database.FileFormat.V2010)) {
80  
81        IndexData.ColumnDescriptor col = getTextIndexColumn(db);
82  
83        // one suffix repeat covers up to 7 chars, so these are the two
84        // boundaries either side of the first extra repeat
85        assertIndexKey("7f7f02010101a0ff0280ff8000", col, repeat('\u3041', 1));
86        assertIndexKey("7f7f02010101ff0280ff8000", col, repeat('\u3042', 1));
87        assertIndexKey(
88            "7f7f027f027f027f027f027f027f027f02010101aaaaa8ff028080ff808000",
89            col, repeat('\u3041', 8));
90        assertIndexKey(
91            "7f7f027f027f027f027f027f027f027f02010101ff028080ff808000",
92            col, repeat('\u3042', 8));
93        assertIndexKey(
94            "7f7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
95            "010101aaaaaaaaaaa0ff02808080ff80808000",
96            col, repeat('\u3041', 16));
97      }
98    }
99  
100   /**
101    * Tests the surrogate chars in the general collation.  The weight table
102    * gives the high surrogates four runs, one of which has no weight at all,
103    * and the low surrogates one piecewise run.  The expected keys were read
104    * back out of the index Access built.
105    */
106   @Test
107   public void testGeneralSurrogates() throws Exception
108   {
109     try(Database db = create(Database.FileFormat.V2010)) {
110 
111       IndexData.ColumnDescriptor col = getTextIndexColumn(db);
112 
113       // U+D800 to U+D83F, primary = cp - 10238, extra byte 0x3f
114       assertIndexKey("7fb002b4f80e020e02013f3f00", col, pair(0x10000) + "aa");
115       assertIndexKey("7fb03fb6fc0e020e02013f3f00", col, pair(0x1F600) + "aa");
116 
117       // U+D840 to U+D87F, primary = cp + 9666, extra byte 0x3e
118       assertIndexKey("7ffe02b4f80e020e02013e3f00", col, pair(0x20000) + "aa");
119       assertIndexKey("7ffe41b8ff0e020e02013e3f00", col, pair(0x2FFFF) + "aa");
120 
121       // U+D880 to U+DB7F, no weight, so the high surrogate writes nothing at
122       // all and the tail carries one extra byte rather than two
123       assertIndexKey("7fb4f80e020e02013f00", col, pair(0x30000) + "aa");
124       assertIndexKey("7fb8ff0e020e02013f00", col, pair(0xEFFFF) + "aa");
125 
126       // U+DB80 to U+DBFF, primary = cp + 9090, extra byte 0x3e
127       assertIndexKey("7fff02b4f80e020e02013e3f00", col, pair(0xF0000) + "aa");
128       assertIndexKey("7fff81b8ff0e020e02013e3f00", col, pair(0x10FFFF) + "aa");
129 
130       // the pair in the other positions, which moves the separator
131       assertIndexKey("7f0e02fe02b4f80e0201023e3f00", col,
132                      "a" + pair(0x20000) + "a");
133       assertIndexKey("7f0e020e02b4f80e020102023f00", col,
134                      "aa" + pair(0x30000) + "a");
135     }
136   }
137 
138   /**
139    * Tests the surrogate chars in the general legacy collation, which gives
140    * them no weight at all, so both halves of a pair are ignored.  The
141    * expected keys were read back out of the index Access built.
142    */
143   @Test
144   public void testGeneralLegacySurrogates() throws Exception
145   {
146     try(Database db = create(Database.FileFormat.V2003)) {
147 
148       IndexData.ColumnDescriptor col = getTextIndexColumn(db);
149 
150       // 4a is the legacy code for 'a'.  every one of these is the key for
151       // "aa", whatever the code point and wherever it sits in the string
152       for(int cp : new int[]{0x10000, 0x20000, 0x30000, 0xF0000, 0x10FFFF}) {
153         assertIndexKey("7f4a4a0100", col, pair(cp) + "aa");
154         assertIndexKey("7f4a4a0100", col, "a" + pair(cp) + "a");
155         assertIndexKey("7f4a4a0100", col, "aa" + pair(cp));
156         assertIndexKey("7f4a4a4a0100", col, "aa" + pair(cp) + "a");
157       }
158     }
159   }
160 
161   private static String pair(int codePoint) {
162     return new String(Character.toChars(codePoint));
163   }
164 
165   /**
166    * Tests that a key longer than the maximum is truncated the way Access
167    * truncates it, with a digest of the discarded bytes.  The expected key was
168    * read back out of the index page Access built.
169    */
170   @Test
171   public void testTruncatedKey() throws Exception
172   {
173     try(Database db = create(Database.FileFormat.V2010)) {
174 
175       IndexData.ColumnDescriptor col = getTextIndexColumn(db);
176 
177       // 200 chars of U+3041 encode to more than the maximum key length, so
178       // Access keeps the leading 508 bytes and ends the key with a digest of
179       // everything it discarded
180       byte[] fullKey = encodeIndexKey(col, repeat('\u3041', 200));
181       assertEquals(533, fullKey.length);
182 
183       // the truncated form is what Access actually stores
184       assertEquals(
185           "7f7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
186           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
187           "027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
188           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
189           "027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
190           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
191           "027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
192           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
193           "027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
194           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
195           "027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f02" +
196           "7f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f027f" +
197           "027f027f02010101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
198           "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
199           "aaaaaaaaaaaaaaaaa8ff0280808080808080808080808080808080808080808080" +
200           "80808080808080ff8080808080d06b",
201           toCompactHex(IndexData.truncateEntryBytes(fullKey)));
202     }
203   }
204 
205   private static IndexData.ColumnDescriptor getTextIndexColumn(Database db)
206     throws Exception
207   {
208     Table t = new TableBuilder("test")
209       .addColumn(new ColumnBuilder("data", DataType.MEMO))
210       .addIndex(new IndexBuilder("dataidx").addColumns("data"))
211       .toTable(db);
212     IndexImpl idx = (IndexImpl)t.getIndex("dataidx");
213     return idx.getIndexData().getColumns().get(0);
214   }
215 
216   private static void assertIndexKey(String expected,
217                                      IndexData.ColumnDescriptor col,
218                                      String value)
219     throws Exception
220   {
221     assertEquals(expected, toCompactHex(encodeIndexKey(col, value)),
222                  "key for " + toUnicodeStr(value));
223   }
224 
225   private static byte[] encodeIndexKey(IndexData.ColumnDescriptor col,
226                                        String value)
227     throws Exception
228   {
229     ByteUtil.ByteStream bout = new ByteUtil.ByteStream();
230     col.writeValue(value, bout);
231     return bout.toByteArray();
232   }
233 
234   private static String repeat(char c, int num)
235   {
236     StringBuilder sb = new StringBuilder(num);
237     for(int i = 0; i < num; ++i) {
238       sb.append(c);
239     }
240     return sb.toString();
241   }
242 
243   private static String toCompactHex(byte[] bytes)
244   {
245     StringBuilder sb = new StringBuilder(bytes.length * 2);
246     for(byte b : bytes) {
247       sb.append(String.format("%02x", b));
248     }
249     return sb.toString();
250   }
251 
252   private static void doTestDb(Basename dbBaseName) throws Exception
253   {
254     for (final TestDB testDB : TestDB.getSupportedForBasename(dbBaseName, true)) {
255       try (Database db = openMem(testDB)) {
256         db.setDateTimeType(DateTimeType.DATE);
257 
258         for(Table t : db) {
259           for(Index index : t.getIndexes()) {
260             checkIndexEntries(testDB, t, index);
261           }
262         }
263       }
264     }
265   }
266 
267   public static void checkIndexEntries(final TestDB testDB, Table t, Index index) throws Exception
268   {
269     Cursor cursor = CursorBuilder.createCursor(index);
270     while(cursor.moveToNextRow()) {
271 
272       Row row = cursor.getCurrentRow();
273 
274       Object data = row.get("data");
275       if((testDB.getExpectedFileFormat() == Database.FileFormat.V1997) &&
276          (data instanceof String) && ((String)data).contains("\uFFFD")) {
277         // this row has a character not supported in the v1997 charset
278         continue;
279       }
280 
281       Cursor.Position curPos = cursor.getSavepoint().getCurrentPosition();
282       boolean success = false;
283       try {
284         findRow(testDB, t, index, row, curPos);
285         success = true;
286       } finally {
287         if(!success) {
288           System.out.println("CurPos: " + curPos);
289           System.out.println("Value: " + row + ": " +
290                              toUnicodeStr(row.get("data")));
291         }
292       }
293     }
294 
295   }
296 
297   private static void findRow(final TestDB testDB, Table t, Index index,
298                               Row expectedRow,
299                               Cursor.Position expectedPos)
300     throws Exception
301   {
302     Object[] idxRow = ((IndexImpl)index).constructIndexRow(expectedRow);
303     Cursor cursor = CursorBuilder.createCursor(index, idxRow, idxRow);
304 
305     Cursor.Position startPos = cursor.getSavepoint().getCurrentPosition();
306 
307     cursor.beforeFirst();
308     while(cursor.moveToNextRow()) {
309       Row row = cursor.getCurrentRow();
310       if(expectedRow.equals(row)) {
311         // verify that the entries are indeed equal
312         Cursor.Position curPos = cursor.getSavepoint().getCurrentPosition();
313         assertEquals(entryToString(expectedPos), entryToString(curPos));
314         return;
315       }
316     }
317 
318     fail("testDB: " + testDB + ";\nCould not find expected row " + expectedRow + " starting at " +
319          entryToString(startPos));
320   }
321 
322   public static String toUnicodeStr(Object obj) {
323     StringBuilder sb = new StringBuilder();
324     for(char c : obj.toString().toCharArray()) {
325       sb.append(toUnicodeStr(c)).append(" ");
326     }
327     return sb.toString();
328   }
329 
330   private static String toUnicodeStr(char c) {
331     String specialStr = SPECIAL_CHARS.get(c);
332     if(specialStr != null) {
333       return specialStr;
334     }
335 
336     String digits = Integer.toHexString(c).toUpperCase();
337     while(digits.length() < 4) {
338       digits = "0" + digits;
339     }
340     return "\\u" + digits;
341   }
342 
343   public static String entryToString(Cursor.Position curPos)
344     throws Exception
345   {
346     Field eField = curPos.getClass().getDeclaredField("_entry");
347     eField.setAccessible(true);
348     IndexData.Entry entry = (IndexData.Entry)eField.get(curPos);
349     Field ebField = entry.getClass().getDeclaredField("_entryBytes");
350     ebField.setAccessible(true);
351     byte[] entryBytes = (byte[])ebField.get(entry);
352 
353     return ByteUtil.toHexString(ByteBuffer.wrap(entryBytes),
354                                 0, entryBytes.length, false);
355   }
356 
357 }