mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(library): skip file write for encrypted PDFs
pdf-lib cannot re-encrypt files, so saving an encrypted PDF strips its encryption but writes back still-encrypted byte streams, silently corrupting the file. Skipping the file write entirely prevents this corruption while allowing DB metadata to persist separately. Fixes #368
This commit is contained in:
@@ -48,12 +48,13 @@ const mockPdfNameOf = PDFName.of as MockedFunction<typeof PDFName.of>;
|
||||
const mockBuildXmp = buildXmp as MockedFunction<typeof buildXmp>;
|
||||
|
||||
describe('PdfFormatWriter', () => {
|
||||
function makePdfDoc() {
|
||||
function makePdfDoc(overrides: { isEncrypted?: boolean } = {}) {
|
||||
const stream = vi.fn().mockReturnValue('stream-ref');
|
||||
const register = vi.fn().mockReturnValue('registered-stream');
|
||||
const set = vi.fn();
|
||||
|
||||
const doc = {
|
||||
isEncrypted: overrides.isEncrypted ?? false,
|
||||
setTitle: vi.fn(),
|
||||
setAuthor: vi.fn(),
|
||||
setSubject: vi.fn(),
|
||||
@@ -145,4 +146,41 @@ describe('PdfFormatWriter', () => {
|
||||
|
||||
expect(mockUnlink).toHaveBeenCalledWith('/books/.tmp-abc-uuid.pdf');
|
||||
});
|
||||
|
||||
it('skips encrypted PDFs without mutating or rewriting the file', async () => {
|
||||
const pdfDoc = makePdfDoc({ isEncrypted: true });
|
||||
mockPdfLoad.mockResolvedValue(pdfDoc as never);
|
||||
|
||||
const writer = new PdfFormatWriter();
|
||||
|
||||
const result = await writer.write('/books/secret.pdf', { title: 'Dune' }, { fieldMask: new Set(['title']), dryRun: false });
|
||||
|
||||
expect(result.status).toBe('skipped');
|
||||
expect(result.reason).toBe('encrypted-pdf');
|
||||
expect(result.fieldsWritten).toEqual([]);
|
||||
expect(typeof result.durationMs).toBe('number');
|
||||
|
||||
// The file must be left untouched: no metadata applied, nothing saved or replaced.
|
||||
expect(pdfDoc.setTitle).not.toHaveBeenCalled();
|
||||
expect(pdfDoc.save).not.toHaveBeenCalled();
|
||||
expect(mockBuildXmp).not.toHaveBeenCalled();
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
expect(mockRename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks encryption before mutating, even when no writable fields are in the mask', async () => {
|
||||
const pdfDoc = makePdfDoc({ isEncrypted: true });
|
||||
mockPdfLoad.mockResolvedValue(pdfDoc as never);
|
||||
|
||||
const writer = new PdfFormatWriter();
|
||||
|
||||
const result = await writer.write('/books/secret.pdf', { title: 'Dune' }, { fieldMask: new Set([]), dryRun: false });
|
||||
|
||||
expect(mockPdfLoad).toHaveBeenCalledWith(Buffer.from('pdf-bytes'), { ignoreEncryption: true });
|
||||
expect(result.status).toBe('skipped');
|
||||
expect(result.reason).toBe('encrypted-pdf');
|
||||
expect(pdfDoc.setCreator).not.toHaveBeenCalled();
|
||||
expect(pdfDoc.save).not.toHaveBeenCalled();
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,13 @@ export class PdfFormatWriter implements FormatWriter {
|
||||
const originalBytes = await readFile(filePath);
|
||||
const pdfDoc = await PDFDocument.load(originalBytes, { ignoreEncryption: true });
|
||||
|
||||
// pdf-lib cannot re-encrypt, so saving an encrypted PDF would strip its
|
||||
// encryption and write back still-encrypted byte streams, silently corrupting
|
||||
// the file. Skip the file write (DB metadata is persisted separately).
|
||||
if (pdfDoc.isEncrypted) {
|
||||
return { status: 'skipped', reason: 'encrypted-pdf', fieldsWritten: [], durationMs: Date.now() - start };
|
||||
}
|
||||
|
||||
applyInfoDict(pdfDoc, payload, pdfFieldMask);
|
||||
embedXmp(pdfDoc, buildXmp(payload, pdfFieldMask));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user