gen_iso9660_testdata.sh (6291B)
1 #!/bin/sh 2 # This file is part of libextractor. 3 # Copyright (C) 2026 Vidyut Samanta and Christian Grothoff 4 # 5 # Regenerate src/plugins/testdata/iso9660_test.iso. 6 # 7 # Requires: python3 (standard library only). 8 # 9 # genisoimage/xorriso are deliberately NOT used. A mastering program 10 # stamps its own name and the current time into the descriptors, which 11 # would make the image non-reproducible and would leave us asserting on 12 # whatever that program happened to write. Hand-building the volume 13 # descriptor set instead lets every field the plugin reads be an exact, 14 # known value, and keeps the image down to the 21 sectors it takes to 15 # hold a system area, four descriptors and one directory extent. 16 # 17 # Layout: 18 # sector 0-15 system area (all zero, mandated by ECMA-119) 19 # sector 16 primary volume descriptor 20 # sector 17 boot record, El Torito 21 # sector 18 supplementary volume descriptor, Joliet (UCS-2 level 3) 22 # sector 19 volume descriptor set terminator 23 # sector 20 root directory extent, with SUSP/Rock Ridge entries 24 set -e 25 26 srcdir=$(dirname "$0")/.. 27 out="$srcdir/src/plugins/testdata/iso9660_test.iso" 28 29 python3 - "$out" <<'EOF' 30 import struct 31 import sys 32 33 SECTOR = 2048 34 out = sys.argv[1] 35 36 SYSTEM_ID = "LINUX" 37 VOLUME_ID = "FORENSIC_TEST_VOL" 38 VOLUME_SET_ID = "FORENSIC_SET_01" 39 PUBLISHER = "GNU LIBEXTRACTOR PROJECT" 40 PREPARER = "LIBEXTRACTOR TEST SUITE" 41 APPLICATION = "MKISOFS 2.01 (LIBEXTRACTOR TEST IMAGE)" 42 COPYRIGHT_FILE = "COPYING.TXT" 43 ABSTRACT_FILE = "ABSTRACT.TXT" 44 BIBLIO_FILE = "BIBLIO.TXT" 45 46 TOTAL_SECTORS = 21 47 ROOT_EXTENT = 20 48 49 50 def astr(text, width): 51 """a-characters / d-characters: space padded, fixed width.""" 52 b = text.encode("ascii") 53 assert len(b) <= width, text 54 return b + b" " * (width - len(b)) 55 56 57 def ucs2(text, width): 58 """Joliet identifiers are UCS-2BE, padded with U+0020.""" 59 b = text.encode("utf-16-be")[:width & ~1] 60 pad = (width - len(b)) // 2 61 return b + b"\x00\x20" * pad + b"\x00" * ((width - len(b)) % 2) 62 63 64 def both16(v): 65 return struct.pack("<H", v) + struct.pack(">H", v) 66 67 68 def both32(v): 69 return struct.pack("<I", v) + struct.pack(">I", v) 70 71 72 def dtime(text): 73 """17-byte dec-datetime, 'YYYYMMDDHHMMSSss' plus a GMT offset byte.""" 74 assert len(text) == 16, text 75 return text.encode("ascii") + b"\x00" 76 77 78 UNSET_DATE = b"0" * 16 + b"\x00" 79 80 # All four dates are pinned; the test asserts on them literally. 81 CREATED = dtime("2024031512345678") # 2024-03-15T12:34:56Z 82 MODIFIED = dtime("2025010203040506") # 2025-01-02T03:04:05Z 83 EXPIRES = dtime("2030060100000000") # 2030-06-01T00:00:00Z 84 EFFECTIVE = dtime("2024031600000000") # 2024-03-16T00:00:00Z 85 86 87 def dir_record(extent, length, flags, ident, system_use=b""): 88 """A directory record; the caller keeps it even-length.""" 89 base = bytearray() 90 base += b"\x00" # length, filled in below 91 base += b"\x00" # extended attribute length 92 base += both32(extent) 93 base += both32(length) 94 # recording date/time: 1980-01-01 00:00:00 GMT 95 base += bytes([80, 1, 1, 0, 0, 0, 0]) 96 base += bytes([flags]) 97 base += b"\x00" # file unit size 98 base += b"\x00" # interleave gap size 99 base += both16(1) # volume sequence number 100 base += bytes([len(ident)]) 101 base += ident 102 if 0 != len(base) % 2: 103 base += b"\x00" # pad the identifier 104 base += system_use 105 assert 0 == len(base) % 2, len(base) 106 base[0] = len(base) 107 return bytes(base) 108 109 110 ROOT_RECORD = dir_record(ROOT_EXTENT, SECTOR, 0x02, b"\x00") 111 assert 34 == len(ROOT_RECORD), len(ROOT_RECORD) 112 113 114 def volume_descriptor(vtype, ident_enc, escape=b""): 115 d = bytearray(SECTOR) 116 d[0] = vtype 117 d[1:6] = b"CD001" 118 d[6] = 1 # version 119 d[7] = 0 # unused / volume flags 120 d[8:40] = ident_enc(SYSTEM_ID, 32) 121 d[40:72] = ident_enc(VOLUME_ID, 32) 122 d[80:88] = both32(TOTAL_SECTORS) 123 d[88:88 + len(escape)] = escape # unused in a PVD 124 d[120:124] = both16(2) # volume set size: two volumes 125 d[124:128] = both16(1) # volume sequence number 126 d[128:132] = both16(SECTOR) # logical block size 127 d[132:140] = both32(10) # path table size 128 d[140:144] = struct.pack("<I", 19) # type-L path table 129 d[148:152] = struct.pack(">I", 19) # type-M path table 130 d[156:190] = ROOT_RECORD 131 d[190:318] = ident_enc(VOLUME_SET_ID, 128) 132 d[318:446] = ident_enc(PUBLISHER, 128) 133 d[446:574] = ident_enc(PREPARER, 128) 134 d[574:702] = ident_enc(APPLICATION, 128) 135 d[702:739] = ident_enc(COPYRIGHT_FILE, 37) 136 d[739:776] = ident_enc(ABSTRACT_FILE, 37) 137 d[776:813] = ident_enc(BIBLIO_FILE, 37) 138 d[813:830] = CREATED 139 d[830:847] = MODIFIED 140 d[847:864] = EXPIRES 141 d[864:881] = EFFECTIVE 142 d[881] = 1 # file structure version 143 return bytes(d) 144 145 146 pvd = volume_descriptor(1, astr) 147 148 # Boot record: El Torito names itself in the boot system identifier. 149 boot = bytearray(SECTOR) 150 boot[0] = 0 151 boot[1:6] = b"CD001" 152 boot[6] = 1 153 bsi = b"EL TORITO SPECIFICATION" 154 boot[7:7 + len(bsi)] = bsi # NUL padded, per the spec 155 boot[71:75] = struct.pack("<I", 20) # boot catalog pointer 156 boot = bytes(boot) 157 158 # Supplementary volume descriptor: Joliet, UCS-2 level 3 ("%/E"). 159 svd = bytearray(volume_descriptor(2, ucs2, escape=b"%/E")) 160 svd = bytes(svd) 161 162 term = bytearray(SECTOR) 163 term[0] = 255 164 term[1:6] = b"CD001" 165 term[6] = 1 166 term = bytes(term) 167 168 # Root directory extent. The "." record carries the SUSP `SP' entry and 169 # a Rock Ridge `RR' entry, which is what the plugin looks for. 170 susp = (b"SP" + bytes([7, 1, 0xBE, 0xEF, 0]) 171 + b"RR" + bytes([5, 1, 0x81])) 172 dot = dir_record(ROOT_EXTENT, SECTOR, 0x02, b"\x00", susp) 173 dotdot = dir_record(ROOT_EXTENT, SECTOR, 0x02, b"\x01") 174 rootdir = bytearray(SECTOR) 175 rootdir[0:len(dot)] = dot 176 rootdir[len(dot):len(dot) + len(dotdot)] = dotdot 177 rootdir = bytes(rootdir) 178 179 image = b"\x00" * (16 * SECTOR) + pvd + boot + svd + term + rootdir 180 assert TOTAL_SECTORS * SECTOR == len(image), len(image) 181 with open(out, "wb") as f: 182 f.write(image) 183 print("wrote %s (%d bytes)" % (out, len(image))) 184 EOF