1from structlog import get_logger
2
3from unblob.extractors import Command
4from unblob.file_utils import Endian
5
6from ...models import (
7 File,
8 HandlerDoc,
9 HandlerType,
10 HexString,
11 Reference,
12 StructHandler,
13 ValidChunk,
14)
15
16logger = get_logger()
17
18
19# The system area, the first 32,768 data bytes of the disc (16 sectors of 2,048 bytes each),
20# is unused by ISO 9660 and therefore available for other uses.
21SYSTEM_AREA_SIZE = 0x8000
22
23
24def from_733(u: bytes) -> int:
25 """Convert from ISO 9660 7.3.3 format to uint32_t.
26
27 Return the little-endian part always, to handle non-specs-compliant images.
28 """
29 return u[0] | (u[1] << 8) | (u[2] << 16) | (u[3] << 24)
30
31
32def from_723(u: bytes) -> int:
33 """Convert from ISO 9660 7.2.3 format to uint16_t.
34
35 Return the little-endian part always, to handle non-specs-compliant images.
36 """
37 return u[0] | (u[1] << 8)
38
39
40class ISO9660FSHandler(StructHandler):
41 NAME = "iso9660"
42
43 #
44 # Match on volume descriptor type, followed by ISO_STANDARD_ID, which corresponds to the beginning of a volume descriptor.
45 #
46 # Volume descriptor types can be:
47 # - 0x00 Boot record volume descriptor
48 # - 0x01 Primary volume descriptor
49 # - 0x02 Supplementary volume descriptor, or enhanced volume descriptor
50 # - 0x03 Volume partition descriptor
51 # - 0xFF Volume descriptor terminator
52
53 PATTERNS = [
54 HexString(
55 "( 00 | 01 | 02 | 03 ) 43 44 30 30 31 // vd_type + 'CD001' (ISO_STANDARD_ID within primary volume descriptor)"
56 )
57 ]
58
59 C_DEFINITIONS = r"""
60 typedef struct iso9660_dtime_s {
61 uint8 dt_year;
62 uint8 dt_month;
63 uint8 dt_day;
64 uint8 dt_hour;
65 uint8 dt_minute;
66 uint8 dt_second;
67 int8 dt_gmtoff;
68 } iso9660_dtime_t;
69
70 typedef struct iso9660_ltime_s {
71 char lt_year[4];
72 char lt_month[2];
73 char lt_day[2];
74 char lt_hour[2];
75 char lt_minute[2];
76 char lt_second[2];
77 char lt_hsecond[2];
78 int8 lt_gmtoff;
79 } iso9660_ltime_t;
80
81 typedef struct iso9660_dir_s {
82 uint8 length;
83 uint8 xa_length;
84 uint64 extent;
85 uint64 size;
86 iso9660_dtime_t recording_time;
87 uint8 file_flags;
88 uint8 file_unit_size;
89 uint8 interleave_gap;
90 uint32 volume_sequence_number;
91 union {
92 uint8 len;
93 char str[1];
94 } filename;
95 } iso9660_dir_t;
96
97 typedef struct iso9660_pvd_s {
98 uint8 type; /**< ISO_VD_PRIMARY - 1 */
99 char id[5]; /**< ISO_STANDARD_ID "CD001"
100 */
101 uint8 version; /**< value 1 for ECMA 119 */
102 char unused1[1]; /**< unused - value 0 */
103 char system_id[32]; /**< each char is an achar */
104 char volume_id[32]; /**< each char is a dchar */
105 uint8 unused2[8]; /**< unused - value 0 */
106 /**uint64 volume_space_size; /**< total number of
107 sectors */
108 uint8 volume_space_size[8];
109 char unused3[32]; /**< unused - value 0 */
110 uint32 volume_set_size; /**< often 1 */
111 uint32 volume_sequence_number; /**< often 1 */
112 uint8 logical_block_size[4]; /**< sector size, e.g. 2048 */
113 } iso9660_pvd_t;
114 """
115
116 HEADER_STRUCT = "iso9660_pvd_t"
117
118 EXTRACTOR = Command("7z", "x", "-y", "{inpath}", "-o{outdir}")
119
120 DOC = HandlerDoc(
121 name="ISO 9660",
122 description="ISO 9660 is a file system standard for optical disc media, defining a volume descriptor structure and directory hierarchy. It is widely used for CD-ROMs and supports cross-platform compatibility.",
123 handler_type=HandlerType.FILESYSTEM,
124 vendor=None,
125 references=[
126 Reference(
127 title="ISO 9660 Specification",
128 url="https://wiki.osdev.org/ISO_9660",
129 ),
130 Reference(
131 title="ISO 9660 Wikipedia",
132 url="https://en.wikipedia.org/wiki/ISO_9660",
133 ),
134 ],
135 limitations=[],
136 )
137
138 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
139 header = self.parse_header(file, Endian.LITTLE)
140 size = from_733(header.volume_space_size) * from_723(header.logical_block_size)
141
142 # We need to subtract the system area given that we matched on volume descriptor,
143 # which is the first struct afterward.
144 real_start_offset = start_offset - SYSTEM_AREA_SIZE
145 if real_start_offset < 0:
146 logger.warning("Invalid ISO 9660 file", offset=real_start_offset, size=size)
147 return None
148
149 return ValidChunk(
150 start_offset=real_start_offset,
151 end_offset=real_start_offset + size,
152 )