diff --git a/package-lock.json b/package-lock.json index 94b01ee..540eff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "mammoth": "^1.12.0", "markdown-it": "^15.0.0", "multer": "^2.2.0", + "node-unrar-js": "^2.0.2", "opentype.js": "^2.0.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", @@ -5307,6 +5308,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-unrar-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-unrar-js/-/node-unrar-js-2.0.2.tgz", + "integrity": "sha512-hLNmoJzqaKJnod8yiTVGe9hnlNRHotUi0CreSv/8HtfRi/3JnRC8DvsmKfeGGguRjTEulhZK6zXX5PXoVuDZ2w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index c6b30b7..32275bb 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "mammoth": "^1.12.0", "markdown-it": "^15.0.0", "multer": "^2.2.0", + "node-unrar-js": "^2.0.2", "opentype.js": "^2.0.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", diff --git a/src/converters/archive.js b/src/converters/archive.js index 4170267..8a6a0eb 100644 --- a/src/converters/archive.js +++ b/src/converters/archive.js @@ -6,6 +6,7 @@ import AdmZip from 'adm-zip'; import { ZipArchive } from 'archiver'; import * as tar from 'tar'; import _7z from '7zip-min'; +import { createExtractorFromFile } from 'node-unrar-js'; import { register } from './registry.js'; const MAX_EXTRACTED_BYTES = 2 * 1024 * 1024 * 1024; @@ -133,6 +134,15 @@ async function createTar7z(srcDir, outputPath, quality) { } } +async function extractRar(inputPath, destDir) { + const extractor = await createExtractorFromFile({ filepath: inputPath, targetPath: destDir }); + const { files } = extractor.extract(); + for (const _file of files) { + // Iterating fully is required: node-unrar-js writes each entry to disk + // lazily as this generator is advanced. + } +} + const EXTRACTORS = { zip: extractZip, tar: extractTarLike, @@ -140,6 +150,7 @@ const EXTRACTORS = { 'tar.bz2': extractTarBz2, '7z': extract7z, 'tar.7z': extractTar7z, + rar: extractRar, }; const CREATORS = { zip: createZip, diff --git a/test/converters/archive.test.js b/test/converters/archive.test.js index 72e3812..6401352 100644 --- a/test/converters/archive.test.js +++ b/test/converters/archive.test.js @@ -228,3 +228,32 @@ describe('archive converters — 7z and tar.7z', () => { 20000 ); }); + +describe('archive converters — rar (extraction only)', () => { + it('lists rar as a source with the other 6 archive formats as targets, and never lists rar as a target for anything', () => { + const targets = listTargetFormats('rar'); + expect(targets.sort()).toEqual(['7z', 'tar', 'tar.7z', 'tar.bz2', 'tar.gz', 'zip'].sort()); + expect(listTargetFormats('zip')).not.toContain('rar'); + }); + + it('converts the sample.rar fixture to zip', async () => { + const rarPath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.rar'); + const outputPath = path.join(tmpDir, 'from-rar.zip'); + const entry = resolve('rar', 'zip'); + + await entry.convert(rarPath, outputPath); + + const outZip = new AdmZip(outputPath); + expect(outZip.getEntries().length).toBeGreaterThan(0); + }); +}); + +describe('archive converters — full registration matrix', () => { + it('registers all 42 source/target pairs (7 sources x 6 targets)', () => { + const sources = ['zip', 'tar', 'tar.gz', 'tar.bz2', '7z', 'tar.7z', 'rar']; + const targets = ['zip', 'tar', 'tar.gz', 'tar.bz2', '7z', 'tar.7z']; + for (const source of sources) { + expect(listTargetFormats(source).sort()).toEqual(targets.sort()); + } + }); +}); diff --git a/test/fixtures/RAR_ATTRIBUTION.md b/test/fixtures/RAR_ATTRIBUTION.md new file mode 100644 index 0000000..8f62039 --- /dev/null +++ b/test/fixtures/RAR_ATTRIBUTION.md @@ -0,0 +1,9 @@ +# RAR fixture attribution + +`sample.rar` is a byte-identical copy of `testFiles/FolderTest.rar` from the +[YuJianrong/node-unrar.js](https://github.com/YuJianrong/node-unrar.js) +repository (MIT-licensed), used there for that project's own extraction +tests. Reused here because there is no way to create a `.rar` fixture from +this codebase — the `unrar` library's license bars any tool (including this +project's own converters) from writing the RAR format, so RAR support is +extraction-only everywhere, including in test fixtures. diff --git a/test/fixtures/sample.rar b/test/fixtures/sample.rar new file mode 100644 index 0000000..c637668 Binary files /dev/null and b/test/fixtures/sample.rar differ