Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/filesystem/ufs.py: 39%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

215 statements  

1import io 

2import stat 

3import struct 

4from collections.abc import Iterator 

5from pathlib import Path 

6 

7from unblob.file_utils import ( 

8 Endian, 

9 FileSystem, 

10 InvalidInputFormat, 

11 StructParser, 

12) 

13from unblob.models import ( 

14 Extractor, 

15 ExtractResult, 

16 File, 

17 HandlerDoc, 

18 HandlerType, 

19 HexString, 

20 Reference, 

21 StructHandler, 

22 ValidChunk, 

23) 

24 

25UFS_C_DEFINITION = """ 

26 #define UFS_MAXMNTLEN 512 

27 #define UFS2_MAXMNTLEN 468 

28 #define UFS2_MAXVOLLEN 32 

29 #define UFS_MAXCSBUFS 31 

30 #define UFS2_NOCSPTRS 28 

31 

32 struct ufs_csum { 

33 uint32 cs_ndir; 

34 uint32 cs_nbfree; 

35 uint32 cs_nifree; 

36 uint32 cs_nffree; 

37 }; 

38 

39 struct ufs2_csum_total { 

40 uint64 cs_ndir; 

41 uint64 cs_nbfree; 

42 uint64 cs_nifree; 

43 uint64 cs_nffree; 

44 uint64 cs_numclusters; 

45 uint64 cs_spare[3]; 

46 }; 

47 

48 struct ufs_timeval { 

49 uint32 tv_sec; 

50 uint32 tv_usec; 

51 }; 

52 

53 struct ufs_super_block { 

54 union { 

55 struct { 

56 uint32 fs_link; /* UNUSED */ 

57 } fs_42; 

58 struct { 

59 uint32 fs_state; /* file system state flag */ 

60 } fs_sun; 

61 } fs_u0; 

62 

63 uint32 fs_rlink; /* UNUSED */ 

64 uint32 fs_sblkno; /* addr of super-block in filesys */ 

65 uint32 fs_cblkno; /* offset of cyl-block in filesys */ 

66 uint32 fs_iblkno; /* offset of inode-blocks in filesys */ 

67 uint32 fs_dblkno; /* offset of first data after cg */ 

68 uint32 fs_cgoffset; /* cylinder group offset in cylinder */ 

69 uint32 fs_cgmask; /* used to calc mod fs_ntrak */ 

70 uint32 fs_time; /* last time written */ 

71 uint32 fs_size; /* number of blocks in fs */ 

72 uint32 fs_dsize; /* number of data blocks in fs */ 

73 uint32 fs_ncg; /* number of cylinder groups */ 

74 uint32 fs_bsize; /* size of basic blocks in fs */ 

75 uint32 fs_fsize; /* size of frag blocks in fs */ 

76 uint32 fs_frag; /* number of frags in a block in fs */ 

77 /* configuration parameters */ 

78 uint32 fs_minfree; /* minimum percentage of free blocks */ 

79 uint32 fs_rotdelay; /* num of ms for optimal next block */ 

80 uint32 fs_rps; /* disk revolutions per second */ 

81 /* fields computed from the others */ 

82 uint32 fs_bmask; /* blkoff calc of blk offsets */ 

83 uint32 fs_fmask; /* fragoff calc of frag offsets */ 

84 uint32 fs_bshift; /* lblkno calc of logical blkno */ 

85 uint32 fs_fshift; /* numfrags calc number of frags */ 

86 /* configuration parameters */ 

87 uint32 fs_maxcontig; /* max number of contiguous blks */ 

88 uint32 fs_maxbpg; /* max number of blks per cyl group */ 

89 /* fields computed from the others */ 

90 uint32 fs_fragshift; /* block to frag shift */ 

91 uint32 fs_fsbtodb; /* fsbtodb and dbtofsb shift constant */ 

92 uint32 fs_sbsize; /* actual size of super block */ 

93 uint32 fs_csmask; /* csum block offset */ 

94 uint32 fs_csshift; /* csum block number */ 

95 uint32 fs_nindir; /* value of NINDIR */ 

96 uint32 fs_inopb; /* value of INOPB */ 

97 uint32 fs_nspf; /* value of NSPF */ 

98 /* optimization preference */ 

99 uint32 fs_optim; 

100 

101 /* fields derived from the hardware */ 

102 union { 

103 struct { 

104 uint32 fs_npsect; /* # sectors/track including spares */ 

105 } fs_sun; 

106 struct { 

107 uint32 fs_state; /* file system state time stamp */ 

108 } fs_sunx86; 

109 } fs_u1; 

110 

111 uint32 fs_interleave; /* hardware sector interleave */ 

112 uint32 fs_trackskew; /* sector 0 skew, per track */ 

113 uint32 fs_id[2]; /* file system id */ 

114 /* sizes determined by number of cylinder groups and their sizes */ 

115 uint32 fs_csaddr; /* blk addr of cyl grp summary area */ 

116 uint32 fs_cssize; /* size of cyl grp summary area */ 

117 uint32 fs_cgsize; /* cylinder group size */ 

118 /* fields derived from the hardware */ 

119 uint32 fs_ntrak; /* tracks per cylinder */ 

120 uint32 fs_nsect; /* sectors per track */ 

121 uint32 fs_spc; /* sectors per cylinder */ 

122 /* this comes from the disk driver partitioning */ 

123 uint32 fs_ncyl; /* cylinders in file system */ 

124 /* fields computed from the others */ 

125 uint32 fs_cpg; /* cylinders per group */ 

126 uint32 fs_inodes_per_group; /* inodes per cylinder group */ 

127 uint32 fs_frags_per_group; /* blocks per group * fs_frag */ 

128 

129 /* this data must be re-computed after crashes */ 

130 struct ufs_csum fs_cstotal; /* cylinder summary information */ 

131 

132 /* fields cleared at mount time */ 

133 int8 fs_fmod; /* super block modified flag */ 

134 int8 fs_clean; /* file system is clean flag */ 

135 int8 fs_ronly; /* mounted read-only flag */ 

136 int8 fs_flags; 

137 

138 union { 

139 struct { 

140 int8 fs_fsmnt[UFS_MAXMNTLEN]; /* name mounted on */ 

141 uint32 fs_cgrotor; /* last cg searched */ 

142 uint32 fs_csp[UFS_MAXCSBUFS]; /* list of fs_cs info buffers */ 

143 uint32 fs_maxcluster; 

144 uint32 fs_cpc; /* cyl per cycle in postbl */ 

145 uint16 fs_opostbl[16][8]; /* old rotation block list head */ 

146 } fs_u1; 

147 struct { 

148 int8 fs_fsmnt[UFS2_MAXMNTLEN]; /* name mounted on */ 

149 uint8 fs_volname[UFS2_MAXVOLLEN]; /* volume name */ 

150 uint64 fs_swuid; /* system-wide uid */ 

151 uint32 fs_pad; /* due to alignment of fs_swuid */ 

152 uint32 fs_cgrotor; /* last cg searched */ 

153 uint32 fs_ocsp[UFS2_NOCSPTRS]; /* list of fs_cs info buffers */ 

154 uint32 fs_contigdirs; /* # of contiguously allocated dirs */ 

155 uint32 fs_csp; /* cg summary info buffer */ 

156 uint32 fs_maxcluster; 

157 uint32 fs_active; /* used by snapshots to track fs */ 

158 uint32 fs_old_cpc; /* cyl per cycle in postbl */ 

159 uint32 fs_maxbsize; /* maximum blocking factor permitted */ 

160 uint64 fs_sparecon64[17]; /* old rotation block list head */ 

161 uint64 fs_sblockloc; /* byte offset of standard superblock */ 

162 struct ufs2_csum_total fs_cstotal; /* cylinder summary information */ 

163 struct ufs_timeval fs_time; /* last time written */ 

164 uint64 fs_size_64; /* number of blocks in fs */ 

165 uint64 fs_dsize; /* number of data blocks in fs */ 

166 uint64 fs_csaddr; /* blk addr of cyl grp summary area */ 

167 uint64 fs_pendingblocks; /* blocks in process of being freed */ 

168 uint32 fs_pendinginodes; /* inodes in process of being freed */ 

169 } fs_u2; 

170 } fs_u11; 

171 

172 union { 

173 struct { 

174 uint32 fs_sparecon[53]; /* reserved for future constants */ 

175 uint32 fs_reclaim; 

176 uint32 fs_sparecon2[1]; 

177 uint32 fs_state; /* file system state time stamp */ 

178 uint32 fs_qbmask[2]; /* ~usb_bmask */ 

179 uint32 fs_qfmask[2]; /* ~usb_fmask */ 

180 } fs_sun; 

181 struct { 

182 uint32 fs_sparecon[53]; /* reserved for future constants */ 

183 uint32 fs_reclaim; 

184 uint32 fs_sparecon2[1]; 

185 uint32 fs_npsect; /* # sectors/track including spares */ 

186 uint32 fs_qbmask[2]; /* ~usb_bmask */ 

187 uint32 fs_qfmask[2]; /* ~usb_fmask */ 

188 } fs_sunx86; 

189 struct { 

190 uint32 fs_sparecon[50]; /* reserved for future constants */ 

191 uint32 fs_contigsumsize; /* size of cluster summary array */ 

192 uint32 fs_maxsymlinklen; /* max length of an internal symlink */ 

193 uint32 fs_inodefmt; /* format of on-disk inodes */ 

194 uint32 fs_maxfilesize[2]; /* max representable file size */ 

195 uint32 fs_qbmask[2]; /* ~usb_bmask */ 

196 uint32 fs_qfmask[2]; /* ~usb_fmask */ 

197 uint32 fs_state; /* file system state time stamp */ 

198 } fs_44; 

199 } fs_u2_arch; 

200 

201 uint32 fs_postblformat; /* format of positional layout tables */ 

202 uint32 fs_nrpos; /* number of rotational positions */ 

203 uint32 fs_postbloff; /* rotation block list head */ 

204 uint32 fs_rotbloff; /* blocks for each rotation */ 

205 uint32 fs_magic; /* magic number */ 

206 uint8 fs_space[1]; /* list of blocks for each rotation */ 

207} ufs_superblock_t; 

208 

209 #define UFS_NDADDR 12 

210 #define UFS_NINDIR 3 

211 

212 /* UFS1 FreeBSD & Solaris */ 

213 struct ufs1_inode { 

214 uint16 mode; /* 0x0 */ 

215 uint16 nlink; /* 0x2 */ 

216 union { 

217 struct { 

218 uint16 suid; /* 0x4 */ 

219 uint16 sgid; /* 0x6 */ 

220 } oldids; 

221 uint32 inumber; /* 0x4 lsf: inode number */ 

222 uint32 author; /* 0x4 GNU HURD: author */ 

223 } u1; 

224 uint64 size; /* 0x8 */ 

225 struct ufs_timeval atime; /* 0x10 access */ 

226 struct ufs_timeval mtime; /* 0x18 modification */ 

227 struct ufs_timeval ctime; /* 0x20 creation */ 

228 union { 

229 struct { 

230 uint32 direct_blocks[UFS_NDADDR];/* 0x28 data blocks */ 

231 uint32 indirect_blocks[UFS_NINDIR];/* 0x58 indirect blocks */ 

232 } addr; 

233 uint8 symlink[4*(UFS_NDADDR+UFS_NINDIR)];/* 0x28 fast symlink */ 

234 } u2; 

235 uint32 flags; /* 0x64 immutable, append-only... */ 

236 uint32 blocks; /* 0x68 blocks in use */ 

237 uint32 gen; /* 0x6c like ext2 i_version, for NFS support */ 

238 union { 

239 struct { 

240 uint32 shadow; /* 0x70 shadow inode with security data */ 

241 uint32 uid; /* 0x74 long EFT version of uid */ 

242 uint32 gid; /* 0x78 long EFT version of gid */ 

243 uint32 oeftflag; /* 0x7c reserved */ 

244 } sun; 

245 struct { 

246 uint32 uid; /* 0x70 File owner */ 

247 uint32 gid; /* 0x74 File group */ 

248 uint32 spare[2]; /* 0x78 reserved */ 

249 } bsd44; 

250 struct { 

251 uint32 uid; /* 0x70 */ 

252 uint32 gid; /* 0x74 */ 

253 uint16 modeh; /* 0x78 mode high bits */ 

254 uint16 spare; /* 0x7A unused */ 

255 uint32 trans; /* 0x7c filesystem translator */ 

256 } hurd; 

257 } u3; 

258 } ufs1_inode_t; 

259 

260 #define UFS_NXADDR 2 

261 

262 /* ---- UFS2 on-disk inode (256 bytes) ---- */ 

263 struct ufs2_inode { 

264 uint16 mode; /* 0: IFMT, permissions; see below. */ 

265 uint16 nlink; /* 2: File link count. */ 

266 uint32 uid; /* 4: File owner. */ 

267 uint32 gid; /* 8: File group. */ 

268 uint32 blksize; /* 12: Inode blocksize. */ 

269 uint64 size; /* 16: File byte count. */ 

270 uint64 blocks; /* 24: Bytes actually held. */ 

271 uint64 atime; /* 32: Last access time. */ 

272 uint64 mtime; /* 40: Last modified time. */ 

273 uint64 ctime; /* 48: Last inode change time. */ 

274 uint64 birthtime; /* 56: Inode creation time. */ 

275 uint32 mtimensec; /* 64: Last modified time. */ 

276 uint32 atimensec; /* 68: Last access time. */ 

277 uint32 ctimensec; /* 72: Last inode change time. */ 

278 uint32 birthnsec; /* 76: Inode creation time. */ 

279 uint32 gen; /* 80: Generation number. */ 

280 uint32 kernflags; /* 84: Kernel flags. */ 

281 uint32 flags; /* 88: Status flags (chflags). */ 

282 uint32 extsize; /* 92: External attributes block. */ 

283 uint64 extb[UFS_NXADDR];/* 96: External attributes block. */ 

284 union { 

285 struct { 

286 uint64 direct_blocks[UFS_NDADDR]; /* 112: Direct disk blocks. */ 

287 uint64 indirect_blocks[UFS_NINDIR];/* 208: Indirect disk blocks.*/ 

288 } addr; 

289 uint8 symlink[2*4*(UFS_NDADDR+UFS_NINDIR)];/* 0x28 fast symlink */ 

290 } u2; 

291 uint64 spare[3]; /* 232: Reserved; currently unused */ 

292 } ufs2_inode_t; 

293 

294 /* New OpenBSD & FreeBSD */ 

295 

296 struct ufs_dirent { 

297 uint32 d_ino; /* inode number of this entry */ 

298 uint16 d_reclen; /* length of this entry */ 

299 uint8 d_type; /* file type */ 

300 uint8 d_namlen; 

301 char d_name[d_namlen]; 

302 }; 

303 

304 /* Solaris & old xBSD (no type field) */ 

305 struct old_ufs_dirent { 

306 uint32 d_ino; 

307 uint16 d_reclen; 

308 uint16 d_namlen; 

309 char d_name[d_namlen]; 

310 } 

311 

312""" 

313 

314MAGIC_OFFSET = 0x55C # relative to SB_OFFSET 

315UFS_ROOT_INO = 2 

316DELETED_INO = 0 

317MAX_BLOCK_SIZE = 65536 # FreeBSD MAXBSIZE (sys/sys/param.h) 

318 

319 

320class UFSParser: 

321 INODE_STRUCT: str 

322 INODE_SIZE: int 

323 PTR_SIZE: int 

324 DIRENT_STRUCT: str = "ufs_dirent" 

325 

326 def __init__(self, file: File, sb_offset: int): 

327 self.file = file 

328 self._struct_parser = StructParser(UFS_C_DEFINITION) 

329 self.file.seek(sb_offset, io.SEEK_SET) 

330 self.super_block = self._struct_parser.parse( 

331 "ufs_superblock_t", self.file, Endian.LITTLE 

332 ) 

333 self.inode_count = ( 

334 self.super_block.fs_inodes_per_group * self.super_block.fs_ncg 

335 ) 

336 

337 def walk_extract(self, fs: FileSystem, ino_num: int, path: Path): # noqa: C901 

338 inode = self.read_inode(ino_num) 

339 file_type = stat.S_IFMT(inode.mode) 

340 

341 match file_type: 

342 case stat.S_IFDIR: 

343 fs.mkdir(path, exist_ok=True) 

344 for child_ino, name in self.parse_dentries(inode): 

345 if name in (".", ".."): 

346 continue 

347 self.walk_extract(fs, child_ino, path / name) 

348 case stat.S_IFREG: 

349 fs.write_chunks(path, self.read_file_content(inode)) 

350 case stat.S_IFLNK: 

351 fs.create_symlink(src=Path(self.read_symlink(inode)), dst=path) 

352 case stat.S_IFIFO: 

353 fs.mkfifo(path) 

354 case stat.S_IFSOCK: 

355 fs.mknod(path, mode=inode.mode) 

356 case stat.S_IFCHR | stat.S_IFBLK: 

357 fs.mknod(path, mode=inode.mode, device=self.get_direct_blocks(inode)[0]) 

358 

359 def parse_dentries(self, inode) -> Iterator[tuple[int, str]]: 

360 for chunk in self.read_file_content(inode): 

361 offset = 0 

362 while offset < len(chunk): 

363 entry = self._struct_parser.parse( 

364 self.DIRENT_STRUCT, chunk[offset:], Endian.LITTLE 

365 ) 

366 # d_reclen == 0 means end of valid entries in this block 

367 if entry.d_reclen == 0: 

368 break 

369 offset += entry.d_reclen 

370 if entry.d_ino == DELETED_INO: 

371 continue 

372 yield ( 

373 entry.d_ino, 

374 entry.d_name.decode("utf-8", errors="replace"), 

375 ) 

376 

377 def read_file_content(self, inode) -> Iterator[bytes]: 

378 remaining = inode.size 

379 for chunk in self.read_direct_blocks(inode, remaining): 

380 yield chunk 

381 remaining -= len(chunk) 

382 if remaining <= 0: 

383 return 

384 for chunk in self.read_indirect_blocks(inode, remaining): 

385 yield chunk 

386 

387 def read_direct_blocks(self, inode, remaining: int) -> Iterator[bytes]: 

388 for fragment_index in self.get_direct_blocks(inode): 

389 if remaining <= 0: 

390 return 

391 to_read = min(self.super_block.fs_bsize, remaining) 

392 if fragment_index == 0: 

393 # Sparse file: unallocated block reads as zeroes 

394 chunk = b"\x00" * to_read 

395 else: 

396 self.seek_data_fragment(fragment_index, to_read) 

397 chunk = self.file.read(to_read) 

398 yield chunk 

399 remaining -= len(chunk) 

400 

401 def read_indirect_blocks(self, inode, remaining: int) -> Iterator[bytes]: # noqa: C901 

402 indirect_blocks = self.get_indirect_blocks(inode) 

403 for level, fragment_index in enumerate(indirect_blocks, start=1): 

404 if fragment_index == 0 or remaining <= 0: 

405 break 

406 # levels: single=1, double=2, triple=3 

407 indexes = [fragment_index] 

408 for _ in range(level): 

409 next_indexes = [] 

410 for idx in indexes: 

411 if idx != 0: 

412 next_indexes.extend(self.read_block_pointers(idx)) 

413 indexes = next_indexes 

414 for data_index in indexes: 

415 if remaining <= 0: 

416 return 

417 to_read = min(self.super_block.fs_bsize, remaining) 

418 if data_index == 0: 

419 # Sparse file: unallocated block reads as zeroes 

420 yield b"\x00" * to_read 

421 else: 

422 self.seek_data_fragment(data_index, to_read) 

423 yield self.file.read(to_read) 

424 remaining -= to_read 

425 

426 def read_block_pointers(self, fragment_index: int) -> list[int]: 

427 self.seek_data_fragment(fragment_index, self.super_block.fs_bsize) 

428 data = self.file.read(self.super_block.fs_bsize) 

429 count = self.super_block.fs_bsize // self.PTR_SIZE 

430 fmt = f"<{count}I" if self.PTR_SIZE == 4 else f"<{count}Q" 

431 return list(struct.unpack(fmt, data)) 

432 

433 def read_symlink(self, inode) -> str: 

434 if inode.blocks == 0: 

435 return bytes(inode.u2.symlink[: inode.size]).decode( 

436 "utf-8", errors="replace" 

437 ) 

438 chunk = next(self.read_file_content(inode)) 

439 return chunk[: inode.size].decode("utf-8", errors="replace") 

440 

441 def read_inode(self, ino_number: int): 

442 if not 1 <= ino_number < self.inode_count: 

443 raise InvalidInputFormat(f"Inode number out of range: {ino_number}") 

444 cylinder_group = ino_number // self.super_block.fs_inodes_per_group 

445 index = ino_number % self.super_block.fs_inodes_per_group 

446 offset = ( 

447 self.frag_to_offset( 

448 self.cylinder_group_start(cylinder_group) + self.super_block.fs_iblkno 

449 ) 

450 + index * self.INODE_SIZE 

451 ) 

452 self.file.seek(offset, io.SEEK_SET) 

453 return self._struct_parser.parse(self.INODE_STRUCT, self.file, Endian.LITTLE) 

454 

455 def frag_to_offset(self, fragment_index: int) -> int: 

456 """Convert a fragment index to a byte offset.""" 

457 return fragment_index * self.super_block.fs_fsize 

458 

459 def total_fragments(self) -> int: 

460 """Return the number of addressable filesystem fragments.""" 

461 return self.super_block.fs_size 

462 

463 def fragments_for_size(self, size: int) -> int: 

464 """Return how many filesystem fragments are needed for size bytes.""" 

465 if size <= 0 or self.super_block.fs_fsize <= 0: 

466 return 0 

467 return (size + self.super_block.fs_fsize - 1) // self.super_block.fs_fsize 

468 

469 def cylinder_group_base(self, cylinder_group: int) -> int: 

470 return cylinder_group * self.super_block.fs_frags_per_group 

471 

472 def is_data_fragment_span(self, fragment_index: int, read_size: int) -> bool: 

473 """Check that a block pointer span stays inside the data area. 

474 

475 Block addresses come straight from inode and indirect-block data, so a 

476 crafted image can point them at the boot block, superblock, cylinder 

477 group headers or inode table, or can start in data and span into the 

478 next cylinder group's metadata or past the end of the filesystem. 

479 """ 

480 frags_per_group = self.super_block.fs_frags_per_group 

481 if frags_per_group <= 0: 

482 return False 

483 fragment_count = self.fragments_for_size(read_size) 

484 if fragment_count <= 0: 

485 return False 

486 

487 end_fragment = fragment_index + fragment_count 

488 if fragment_index <= 0 or end_fragment > self.total_fragments(): 

489 return False 

490 

491 cylinder_group = fragment_index // frags_per_group 

492 group_end = self.cylinder_group_base(cylinder_group) + frags_per_group 

493 data_start = ( 

494 self.cylinder_group_start(cylinder_group) + self.super_block.fs_dblkno 

495 ) 

496 return fragment_index >= data_start and end_fragment <= group_end 

497 

498 def seek_data_fragment(self, fragment_index: int, read_size: int): 

499 """Validate a block pointer, then seek to its data.""" 

500 if not self.is_data_fragment_span(fragment_index, read_size): 

501 raise InvalidInputFormat( 

502 f"Block pointer outside the data area: {fragment_index}" 

503 ) 

504 self.file.seek(self.frag_to_offset(fragment_index), io.SEEK_SET) 

505 

506 def cylinder_group_start(self, cylinder_group: int) -> int: 

507 return self.cylinder_group_base(cylinder_group) 

508 

509 def get_direct_blocks(self, inode) -> list[int]: 

510 return inode.u2.addr.direct_blocks 

511 

512 def get_indirect_blocks(self, inode) -> list[int]: 

513 return inode.u2.addr.indirect_blocks 

514 

515 

516class UFS1Parser(UFSParser): 

517 INODE_STRUCT = "ufs1_inode_t" 

518 INODE_SIZE = 128 

519 PTR_SIZE = 4 

520 

521 def cylinder_group_start(self, cylinder_group: int) -> int: 

522 # Old UFS1 rotates cylinder group layout to minimize seek time on spinning disks 

523 return ( 

524 cylinder_group * self.super_block.fs_frags_per_group 

525 + self.super_block.fs_cgoffset 

526 * (cylinder_group & ~self.super_block.fs_cgmask) 

527 ) 

528 

529 

530class UFS2Parser(UFSParser): 

531 INODE_STRUCT = "ufs2_inode_t" 

532 INODE_SIZE = 256 

533 PTR_SIZE = 8 

534 

535 def total_fragments(self) -> int: 

536 return self.super_block.fs_u11.fs_u2.fs_size_64 

537 

538 

539class SolarisUFS1Parser(UFS1Parser): 

540 DIRENT_STRUCT = "old_ufs_dirent" 

541 

542 

543class UFSExtractor(Extractor): 

544 def __init__(self, parser: type[UFSParser], sb_offset: int): 

545 self.parser = parser 

546 self.sb_offset = sb_offset 

547 

548 def extract(self, inpath: Path, outdir: Path): 

549 fs = FileSystem(outdir) 

550 with File.from_path(inpath) as f: 

551 parser = self.parser(f, self.sb_offset) 

552 parser.walk_extract(fs, UFS_ROOT_INO, Path("/")) 

553 return ExtractResult(reports=fs.problems) 

554 

555 

556class _UFSBaseHandler(StructHandler): 

557 HEADER_STRUCT = "ufs_superblock_t" 

558 C_DEFINITIONS = UFS_C_DEFINITION 

559 EXTRACTOR = None 

560 SB_OFFSET = 0 

561 

562 def get_fragment_count(self, header) -> int: 

563 raise NotImplementedError("Subclasses must implement this function.") 

564 

565 def is_valid_header(self, header) -> bool: 

566 return ( 

567 header.fs_fsize > 0 

568 and header.fs_bsize > 0 

569 and header.fs_bsize <= MAX_BLOCK_SIZE 

570 and header.fs_frag == (header.fs_bsize // header.fs_fsize) 

571 and self.get_fragment_count(header) > 0 

572 and header.fs_ncg > 0 

573 ) 

574 

575 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None: 

576 file.seek( 

577 start_offset + self.SB_OFFSET, io.SEEK_SET 

578 ) # Skip the boot sector to reach the start of UFS superblock 

579 header = self.parse_header(file) 

580 if not self.is_valid_header(header): 

581 raise InvalidInputFormat("Invalid UFS Header") 

582 

583 end_offset = start_offset + (self.get_fragment_count(header) * header.fs_fsize) 

584 return ValidChunk(start_offset=start_offset, end_offset=end_offset) 

585 

586 

587class UFS1Handler(_UFSBaseHandler): 

588 NAME = "ufs1" 

589 PATTERNS = [ 

590 HexString("( 01 | 02 ) 00 00 00 [8] 54 19 01 00") 

591 ] # fs_nrpos + UFS1 fs_magic + null fs_space 

592 SB_OFFSET = 0x2000 

593 EXTRACTOR = UFSExtractor(UFS1Parser, SB_OFFSET) 

594 PATTERN_MATCH_OFFSET = -SB_OFFSET - (MAGIC_OFFSET - 12) 

595 DOC = HandlerDoc( 

596 name="ufs1", 

597 description="UFS1 (Unix File System 1) is the original UFS implementation supported by Unix-like operating systems such as FreeBSD and Solaris. It utilizes a hierarchical tree structure and inodes to manage file metadata and data block addresses, with 32-bit block addressing limiting partition sizes to 1TB.", 

598 handler_type=HandlerType.FILESYSTEM, 

599 vendor=None, 

600 references=[ 

601 Reference( 

602 title="Unix File System Wikipedia", 

603 url="https://en.wikipedia.org/wiki/Unix_File_System", 

604 ) 

605 ], 

606 limitations=[], 

607 ) 

608 

609 def get_fragment_count(self, header) -> int: 

610 return header.fs_size 

611 

612 

613class UFS2Handler(_UFSBaseHandler): 

614 NAME = "ufs2" 

615 PATTERNS = [HexString("19 01 54 19 00")] # UFS2 fs_magic + null fs_space 

616 SB_OFFSET = 0x10000 

617 PATTERN_MATCH_OFFSET = -SB_OFFSET - MAGIC_OFFSET 

618 EXTRACTOR = UFSExtractor(UFS2Parser, SB_OFFSET) 

619 DOC = HandlerDoc( 

620 name="ufs2", 

621 description="UFS2 (Unix File System 2) is an extended version of UFS1 supported by Unix-like operating systems such as FreeBSD. It introduces 64-bit block addressing, extended file attributes, and improved performance over UFS1, while retaining the hierarchical tree structure and inodes for file metadata and data block management.", 

622 handler_type=HandlerType.FILESYSTEM, 

623 vendor=None, 

624 references=[ 

625 Reference( 

626 title="Unix File System Wikipedia", 

627 url="https://en.wikipedia.org/wiki/Unix_File_System", 

628 ) 

629 ], 

630 limitations=[], 

631 ) 

632 

633 def get_fragment_count(self, header) -> int: 

634 return header.fs_u11.fs_u2.fs_size_64 

635 

636 

637class SolarisHandler(_UFSBaseHandler): 

638 NAME = "solaris_ufs1" 

639 PATTERNS = [HexString("08 00 00 00 [8] 54 19 01 00")] 

640 SB_OFFSET = 0x2000 

641 EXTRACTOR = UFSExtractor(SolarisUFS1Parser, SB_OFFSET) 

642 PATTERN_MATCH_OFFSET = -SB_OFFSET - (MAGIC_OFFSET - 12) 

643 DOC = HandlerDoc( 

644 name="solaris_ufs1", 

645 description="Solaris UFS1 is the variant of UFS1 used by Oracle Solaris and illumos-based systems such as OpenIndiana and OmniOS. It shares the same overall design as FreeBSD UFS1 but keeps an older on-disk convention inherited from early Unix, making it incompatible with FreeBSD's variant despite the shared magic number.", 

646 handler_type=HandlerType.FILESYSTEM, 

647 vendor=None, 

648 references=[ 

649 Reference( 

650 title="Unix File System Wikipedia", 

651 url="https://en.wikipedia.org/wiki/Unix_File_System", 

652 ), 

653 Reference( 

654 title="Oracle Solaris", 

655 url="https://en.wikipedia.org/wiki/Oracle_Solaris", 

656 ), 

657 ], 

658 limitations=[], 

659 ) 

660 

661 def get_fragment_count(self, header) -> int: 

662 return header.fs_size