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