libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

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