|
16 | 16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
17 | 17 | */ |
18 | 18 |
|
| 19 | +import {BlobWriter, TextReader, ZipWriter} from '@zip.js/zip.js'; |
19 | 20 | import {IDBFactory, IDBKeyRange} from 'fake-indexeddb'; |
20 | 21 | import {readFileSync} from 'node:fs'; |
21 | 22 | import {fileURLToPath} from 'node:url'; |
@@ -43,6 +44,22 @@ async function createTestDictionaryArchiveData(dictionary, dictionaryName) { |
43 | 44 | return await createDictionaryArchiveData(dictionaryDirectory, dictionaryName); |
44 | 45 | } |
45 | 46 |
|
| 47 | +/** |
| 48 | + * Creates a dictionary zip archive with raw file contents, bypassing JSON parse/re-serialize. |
| 49 | + * This allows testing with intentionally malformed JSON that parseJson would reject. |
| 50 | + * @param {Record<string, string>} files Map of filename to raw string content |
| 51 | + * @returns {Promise<ArrayBuffer>} |
| 52 | + */ |
| 53 | +async function createRawDictionaryArchiveData(files) { |
| 54 | + const zipFileWriter = new BlobWriter(); |
| 55 | + const zipWriter = new ZipWriter(zipFileWriter, {level: 0}); |
| 56 | + for (const [fileName, content] of Object.entries(files)) { |
| 57 | + await zipWriter.add(fileName, new TextReader(content)); |
| 58 | + } |
| 59 | + const blob = await zipWriter.close(); |
| 60 | + return await blob.arrayBuffer(); |
| 61 | +} |
| 62 | + |
46 | 63 | /** |
47 | 64 | * @param {import('vitest').ExpectStatic} expect |
48 | 65 | * @param {import('dictionary-importer').OnProgressCallback} [onProgress] |
@@ -179,6 +196,41 @@ describe('Database', () => { |
179 | 196 | }); |
180 | 197 | }); |
181 | 198 | }); |
| 199 | + describe('Invalid raw dictionaries', () => { |
| 200 | + const indexJson = JSON.stringify({title: 'Raw Test', format: 3, revision: 'test', sequenced: true}); |
| 201 | + const validEntry = '["打","だ","n","n",1,["definition"],1,""]'; |
| 202 | + const rawInvalidDictionaries = [ |
| 203 | + {name: 'missing comma between entries', termBank: `[${validEntry}${validEntry}]`}, |
| 204 | + {name: 'leading comma', termBank: `[,${validEntry}]`}, |
| 205 | + {name: 'double comma', termBank: `[${validEntry},,${validEntry}]`}, |
| 206 | + {name: 'trailing garbage after array', termBank: `[${validEntry}]garbage`}, |
| 207 | + {name: 'leading garbage before array', termBank: `garbage[${validEntry}]`}, |
| 208 | + {name: 'concatenated arrays', termBank: `[${validEntry}][${validEntry}]`}, |
| 209 | + {name: 'empty file', termBank: ''}, |
| 210 | + {name: 'whitespace only', termBank: ' '}, |
| 211 | + {name: 'just a number', termBank: '123'}, |
| 212 | + {name: 'just a string', termBank: '"hello"'}, |
| 213 | + {name: 'just null', termBank: 'null'}, |
| 214 | + {name: 'unclosed array', termBank: `[${validEntry}`}, |
| 215 | + {name: 'unclosed entry', termBank: '[["a","b"'}, |
| 216 | + ]; |
| 217 | + describe.each(rawInvalidDictionaries)('Raw invalid: $name', ({termBank}) => { |
| 218 | + test('Has invalid data', async ({expect}) => { |
| 219 | + const dictionaryDatabase = new DictionaryDatabase(); |
| 220 | + await dictionaryDatabase.prepare(); |
| 221 | + |
| 222 | + const testDictionarySource = await createRawDictionaryArchiveData({ |
| 223 | + 'index.json': indexJson, |
| 224 | + 'term_bank_1.json': termBank, |
| 225 | + }); |
| 226 | + |
| 227 | + /** @type {import('dictionary-importer').ImportDetails} */ |
| 228 | + const importDetails = {prefixWildcardsSupported: false, yomitanVersion: '0.0.0.0'}; |
| 229 | + await expect.soft(createDictionaryImporter(expect).importDictionary(dictionaryDatabase, testDictionarySource, importDetails)).rejects.toThrow('Dictionary has invalid data'); |
| 230 | + await dictionaryDatabase.close(); |
| 231 | + }); |
| 232 | + }); |
| 233 | + }); |
182 | 234 | describe('Database valid usage', () => { |
183 | 235 | const testDataFilePath = join(dirname, 'data/database-test-cases.json'); |
184 | 236 | /** @type {import('test/database').DatabaseTestData} */ |
|
0 commit comments