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