Books: read DRM-free MOBI, AZW and AZW3; refuse Kindle files with DRM

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
Claude committed 2026-10-24 02:54:34 +00:00
1 parent 07c16ddc25
commit 2e99893c8b
7 files changed
+138 -21

No files matched your search

+6 -2
View File
@@ -15,7 +15,11 @@ const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] ==
r = await up('d.docx', docx); assert.equal(r.status, 200);
assert.equal((await up('fake.pdf', Buffer.from('<html>not a pdf</html>'))).status, 400, 'a file must look like what its name says');
assert.equal((await up('fake.epub', pdf)).status, 400); assert.equal((await up('x.exe', epub)).status, 400); assert.equal((await up('plain.docx', epub)).status, 400, 'an EPUB is not a DOCX');
assert.equal((await call('/list')).status, 200); assert.equal((await (await call('/list')).json()).length, 3);
const mobi = fs.readFileSync(path.join(fx, 'test.mobi')), drm = fs.readFileSync(path.join(fx, 'drm.mobi'));
r = await up('k.azw3', mobi); assert.equal(r.status, 200); assert.equal((await r.json()).ext, 'azw3', 'DRM-free Kindle files are accepted');
r = await up('locked.mobi', drm); assert.equal(r.status, 400); assert.match((await r.json()).error, /DRM/, 'books with DRM are refused with a clear reason');
assert.equal((await up('fake.mobi', pdf)).status, 400, 'a MOBI must look like one');
assert.equal((await call('/list')).status, 200); assert.equal((await (await call('/list')).json()).length, 4);
assert.equal((await (await call('/list', { headers: { 'x-user': 'b' } })).json()).length, 0, 'each person has their own shelf');
assert.equal((await call('/book/' + e.id + '/file', { headers: { 'x-user': 'b' } })).status, 404, "nobody can open another person's book");
r = await call('/book/' + p.id + '/file', { headers: { range: 'bytes=0-4' } }); assert.equal(r.status, 206); assert.equal(await r.text(), '%PDF-'); assert.equal(r.headers.get('content-range'), `bytes 0-4/${pdf.length}`);
@@ -34,7 +38,7 @@ const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] ==
assert.deepEqual(ns.map(n => n.id).sort(), ['n1', 'n2'], 'two devices add notes without losing each other'); assert.equal(ns.find(n => n.id === 'n1').text, 'hello', 'an older edit does not win');
ns = await note([{ id: 'n1', t: 20, del: true, loc: 'y' }]); assert.deepEqual(ns.map(n => n.id), ['n2'], 'deleted notes stay deleted');
assert.equal((await (await call('/book/' + e.id + '/notes')).json()).length, 1);
assert.equal((await call('/book/' + e.id, { method: 'DELETE' })).status, 200); assert.equal((await (await call('/list')).json()).length, 2);
assert.equal((await call('/book/' + e.id, { method: 'DELETE' })).status, 200); assert.equal((await (await call('/list')).json()).length, 3);
assert.equal((await call('/book/' + e.id + '/file')).status, 404);
assert.equal((await call('/nope')).status, 404);
console.log('books test ok');
+30
View File
@@ -22,3 +22,33 @@ data = '%PDF-1.4\n'; offs = []
for i, o in enumerate(objs): offs.append(len(data)); data += f'{i+1} 0 obj\n{o}\nendobj\n'
x = len(data); data += f'xref\n0 {len(objs)+1}\n0000000000 65535 f \n' + ''.join(f'{o:010d} 00000 n \n' for o in offs) + f'trailer\n<< /Size {len(objs)+1} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n'
open(os.path.join(out, 'test.pdf'), 'w').write(data)
# minimal MOBI 6 (uncompressed text, no images), enough to open in a reader. Set DRM=1 in the environment to mark it encrypted.
import struct
def mkmobi(path, drm=False):
html = '<html><head><title>Mobi Rabbits</title></head><body><h1>Chapter One</h1>' + ''.join('<p>The rabbit hops through the clover field number %d, nibbling as it goes.</p>' % i for i in range(30)) + '<mbp:pagebreak/><h1>Chapter Two</h1>' + ''.join('<p>Carrots are the second chapter topic, paragraph %d.</p>' % i for i in range(30)) + '</body></html>'
text = html.encode('utf8'); recs = [text[i:i+4096] for i in range(0, len(text), 4096)]
title = b'Mobi Rabbits'; hdrlen = 232
exth = b''
def item(t, v): return struct.pack('>II', t, 8 + len(v)) + v
body = item(100, b'M. Author') + item(503, title); exth = b'EXTH' + struct.pack('>II', 12 + len(body), 2) + body
exth += b'\0' * (-len(exth) % 4)
r0 = struct.pack('>HHIHHHH', 1, 0, len(text), len(recs), 4096, 2 if drm else 0, 0)
mobi = bytearray(hdrlen)
mobi[0:4] = b'MOBI'; struct.pack_into('>I', mobi, 4, hdrlen); struct.pack_into('>I', mobi, 8, 2); struct.pack_into('>I', mobi, 12, 65001); struct.pack_into('>I', mobi, 16, 1234); struct.pack_into('>I', mobi, 20, 6)
for off in range(24, 64, 4): struct.pack_into('>I', mobi, off, 0xFFFFFFFF)
name_off = 16 + hdrlen + len(exth)
struct.pack_into('>II', mobi, 68 - 16, name_off, len(title)); struct.pack_into('>I', mobi, 72 - 16 + 4, 9)
struct.pack_into('>I', mobi, 0x5C - 16 + 0, 0)
struct.pack_into('>I', mobi, 0x6C - 16, 0xFFFFFFFF); struct.pack_into('>I', mobi, 0x70 - 16, 0xFFFFFFFF)
struct.pack_into('>I', mobi, 0x80 - 16, 0x40)
for off in (0x90 - 16, 0x94 - 16, 0x98 - 16, 0xA0 - 16): struct.pack_into('>I', mobi, off, 0xFFFFFFFF)
struct.pack_into('>I', mobi, 0xE0 - 16, 0xFFFFFFFF); struct.pack_into('>H', mobi, 0xF2 - 16, 0)
rec0 = r0 + bytes(mobi) + exth + title + b'\0\0'
allrecs = [rec0] + recs; n = len(allrecs); off = 78 + 8 * n + 2; offs = []
for r in allrecs: offs.append(off); off += len(r)
hdr = b'MobiRabbits'.ljust(32, b'\0') + struct.pack('>HHIIIIII', 0, 0, 0, 0, 0, 0, 0, 0) + b'BOOKMOBI' + struct.pack('>IIH', 1234, 0, n)
hdr = hdr[:78 - 0] if len(hdr) >= 78 else hdr.ljust(78, b'\0')
lst = b''.join(struct.pack('>IBBBB', o, 0, 0, 0, i & 255) for i, o in enumerate(offs))
open(path, 'wb').write(hdr + lst + b'\0\0' + b''.join(allrecs))
mkmobi(os.path.join(out, 'test.mobi')); mkmobi(os.path.join(out, 'drm.mobi'), True)