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:
7 files changed
+138
-21
No files matched your search
+6
-2
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user