feat: add ebook MIME types, fix azw3/mobi detection collision, trust undetectable ebook formats
This commit is contained in:
+20
-1
@@ -22,6 +22,15 @@ const OUTPUT_MIME_TYPES = {
|
|||||||
ttf: 'font/ttf',
|
ttf: 'font/ttf',
|
||||||
otf: 'font/otf',
|
otf: 'font/otf',
|
||||||
woff: 'font/woff',
|
woff: 'font/woff',
|
||||||
|
epub: 'application/epub+zip',
|
||||||
|
fb2: 'application/x-fictionbook+xml',
|
||||||
|
lrf: 'application/octet-stream',
|
||||||
|
mobi: 'application/x-mobipocket-ebook',
|
||||||
|
pdb: 'application/vnd.palm',
|
||||||
|
rb: 'application/octet-stream',
|
||||||
|
snb: 'application/octet-stream',
|
||||||
|
tcr: 'application/octet-stream',
|
||||||
|
azw3: 'application/vnd.amazon.ebook',
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function detectInputMime(filePath) {
|
export async function detectInputMime(filePath) {
|
||||||
@@ -44,9 +53,19 @@ const UNDETECTABLE_TEXT_FORMATS = {
|
|||||||
csv: 'text/csv',
|
csv: 'text/csv',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UNDETECTABLE_EBOOK_FORMATS = {
|
||||||
|
fb2: 'application/x-fictionbook+xml',
|
||||||
|
lrf: 'application/octet-stream',
|
||||||
|
pdb: 'application/vnd.palm',
|
||||||
|
rb: 'application/octet-stream',
|
||||||
|
snb: 'application/octet-stream',
|
||||||
|
tcr: 'application/octet-stream',
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeFormat(format) {
|
function normalizeFormat(format) {
|
||||||
if (format === 'jpg') return 'jpeg';
|
if (format === 'jpg') return 'jpeg';
|
||||||
if (format === 'heif') return 'heic';
|
if (format === 'heif') return 'heic';
|
||||||
|
if (format === 'azw3') return 'mobi';
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +80,7 @@ export async function resolveInputFormat(filePath, declaredFormat) {
|
|||||||
const detected = await detectInputMime(filePath);
|
const detected = await detectInputMime(filePath);
|
||||||
|
|
||||||
if (!detected) {
|
if (!detected) {
|
||||||
const fallbackMime = UNDETECTABLE_TEXT_FORMATS[declaredFormat];
|
const fallbackMime = UNDETECTABLE_TEXT_FORMATS[declaredFormat] ?? UNDETECTABLE_EBOOK_FORMATS[declaredFormat];
|
||||||
if (fallbackMime) {
|
if (fallbackMime) {
|
||||||
return { mime: fallbackMime, valid: true };
|
return { mime: fallbackMime, valid: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,85 @@ describe('outputMimeType — fonts', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('outputMimeType — ebooks', () => {
|
||||||
|
it('returns the correct MIME type for each ebook target format', () => {
|
||||||
|
expect(outputMimeType('epub')).toBe('application/epub+zip');
|
||||||
|
expect(outputMimeType('fb2')).toBe('application/x-fictionbook+xml');
|
||||||
|
expect(outputMimeType('mobi')).toBe('application/x-mobipocket-ebook');
|
||||||
|
expect(outputMimeType('azw3')).toBe('application/vnd.amazon.ebook');
|
||||||
|
expect(outputMimeType('pdb')).toBe('application/vnd.palm');
|
||||||
|
expect(outputMimeType('lrf')).toBe('application/octet-stream');
|
||||||
|
expect(outputMimeType('rb')).toBe('application/octet-stream');
|
||||||
|
expect(outputMimeType('snb')).toBe('application/octet-stream');
|
||||||
|
expect(outputMimeType('tcr')).toBe('application/octet-stream');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveInputFormat — azw3/mobi collision', () => {
|
||||||
|
function buildMobiFamilyBuffer() {
|
||||||
|
// PDB header: 8-byte "BOOKMOBI" type+creator magic at offset 60, shared by both
|
||||||
|
// real .mobi and real .azw3 files (confirmed in file-type's source).
|
||||||
|
const buffer = Buffer.alloc(68);
|
||||||
|
buffer.write('BOOKMOBI', 60, 'ascii');
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('confirms file-type reports mobi-family content as ext "mobi" regardless of which of the two formats it is (documents the collision this fix works around)', async () => {
|
||||||
|
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-mobi-family.bin');
|
||||||
|
await fs.writeFile(fixturePath, buildMobiFamilyBuffer());
|
||||||
|
|
||||||
|
const detected = await detectInputMime(fixturePath);
|
||||||
|
expect(detected).toEqual({ ext: 'mobi', mime: 'application/x-mobipocket-ebook' });
|
||||||
|
|
||||||
|
await fs.unlink(fixturePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a real mobi-family file declared as mobi', async () => {
|
||||||
|
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-mobi-family.bin');
|
||||||
|
await fs.writeFile(fixturePath, buildMobiFamilyBuffer());
|
||||||
|
|
||||||
|
const result = await resolveInputFormat(fixturePath, 'mobi');
|
||||||
|
expect(result).toEqual({ mime: 'application/x-mobipocket-ebook', valid: true });
|
||||||
|
|
||||||
|
await fs.unlink(fixturePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a real mobi-family file declared as azw3 despite file-type reporting it as mobi', async () => {
|
||||||
|
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-mobi-family.bin');
|
||||||
|
await fs.writeFile(fixturePath, buildMobiFamilyBuffer());
|
||||||
|
|
||||||
|
const result = await resolveInputFormat(fixturePath, 'azw3');
|
||||||
|
expect(result).toEqual({ mime: 'application/x-mobipocket-ebook', valid: true });
|
||||||
|
|
||||||
|
await fs.unlink(fixturePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveInputFormat — undetectable ebook formats', () => {
|
||||||
|
const undetectableFormats = {
|
||||||
|
fb2: 'application/x-fictionbook+xml',
|
||||||
|
lrf: 'application/octet-stream',
|
||||||
|
pdb: 'application/vnd.palm',
|
||||||
|
rb: 'application/octet-stream',
|
||||||
|
snb: 'application/octet-stream',
|
||||||
|
tcr: 'application/octet-stream',
|
||||||
|
};
|
||||||
|
|
||||||
|
it.each(Object.entries(undetectableFormats))(
|
||||||
|
'trusts the declared format for undetectable %s files',
|
||||||
|
async (declaredFormat, expectedMime) => {
|
||||||
|
const fixturePath = path.join(import.meta.dirname, 'fixtures', `sample.${declaredFormat}`);
|
||||||
|
await fs.writeFile(fixturePath, 'arbitrary bytes with no recognizable magic number');
|
||||||
|
|
||||||
|
const result = await resolveInputFormat(fixturePath, declaredFormat);
|
||||||
|
|
||||||
|
expect(result).toEqual({ mime: expectedMime, valid: true });
|
||||||
|
|
||||||
|
await fs.unlink(fixturePath);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('resolveInputFormat — dfont', () => {
|
describe('resolveInputFormat — dfont', () => {
|
||||||
it('confirms file-type alone misidentifies the dfont fixture as ico (documents the bug this fix works around)', async () => {
|
it('confirms file-type alone misidentifies the dfont fixture as ico (documents the bug this fix works around)', async () => {
|
||||||
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.dfont');
|
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.dfont');
|
||||||
|
|||||||
Reference in New Issue
Block a user