Skip to main content

ufs/
cg.rs

1//! UFS/FFS cylinder-group header (`struct cg`).
2//!
3//! Every cylinder group carries a header (`struct cg`, magic `0x00090255`)
4//! holding the per-group allocation state: how many data blocks/inodes it owns
5//! and the byte offsets (from the header start) of the used-inode and free-block
6//! bitmaps that P1 uses to tell allocated from free/deleted inodes. Field
7//! offsets follow `struct cg` in `sys/ufs/ffs/fs.h`, verified against the four
8//! cg headers in the dfvfs `ufs2.raw` image (see `docs/RESEARCH.md` §1).
9
10use crate::bytes::Endian;
11use crate::error::UfsError;
12
13/// The cylinder-group header magic (`CG_MAGIC`), at offset 4 of the header.
14pub const CG_MAGIC: u32 = 0x0009_0255;
15
16/// `cg_magic` is at offset 4 (`cg_firstfield` occupies bytes 0..4).
17const OFF_MAGIC: usize = 4;
18const OFF_CGX: usize = 12;
19const OFF_NDBLK: usize = 20;
20const OFF_IUSEDOFF: usize = 92;
21const OFF_FREEOFF: usize = 96;
22const OFF_CLUSTEROFF: usize = 108;
23const OFF_NIBLK: usize = 116;
24const OFF_INITEDIBLK: usize = 120;
25
26/// Minimum bytes required to read the header fields (through `cg_initediblk` at
27/// offset 120, +4).
28const CG_MIN_LEN: usize = 124;
29
30/// Parsed cylinder-group header — the per-group allocation map.
31///
32/// Carries the subset of `struct cg` P1 needs; `#[non_exhaustive]` so later
33/// phases add fields without a breaking change. The bitmaps themselves are not
34/// copied here — [`Self::inosused_off`] / [`Self::blksfree_off`] give the byte
35/// offsets into the header buffer where they live.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct CylinderGroup {
39    /// `cg_cgx` — this cylinder group's index.
40    pub cgx: u32,
41    /// `cg_ndblk` — number of data blocks in this cylinder group.
42    pub ndblk: u32,
43    /// `cg_niblk` — number of inode blocks in this cylinder group.
44    pub niblk: i32,
45    /// `cg_initediblk` — number of initialized inodes in this cg (UFS2).
46    pub initediblk: u32,
47    /// `cg_iusedoff` — byte offset (from the header start) of the used-inode
48    /// bitmap (`cg_inosused`).
49    pub iusedoff: u32,
50    /// `cg_freeoff` — byte offset (from the header start) of the free-block
51    /// bitmap (`cg_blksfree`).
52    pub freeoff: u32,
53    /// `cg_clusteroff` — byte offset of the free-cluster bitmap.
54    pub clusteroff: u32,
55}
56
57impl CylinderGroup {
58    /// Parse a cylinder-group header from the start of `data` (i.e. `data`
59    /// begins at the `struct cg` header), using the byte order resolved from the
60    /// superblock.
61    ///
62    /// # Errors
63    ///
64    /// - [`UfsError::Truncated`] if `data` is shorter than the header fields.
65    /// - [`UfsError::BadCgMagic`] if `cg_magic` (offset 4) is not `0x00090255`
66    ///   in the given byte order — the offending value is carried.
67    pub fn parse(data: &[u8], endian: Endian) -> Result<Self, UfsError> {
68        if data.len() < CG_MIN_LEN {
69            return Err(UfsError::Truncated {
70                structure: "cylinder group",
71                need: CG_MIN_LEN,
72                have: data.len(),
73            });
74        }
75        let magic = endian.u32(data, OFF_MAGIC);
76        if magic != CG_MAGIC {
77            return Err(UfsError::BadCgMagic {
78                found: magic,
79                endian,
80            });
81        }
82        Ok(Self {
83            cgx: endian.u32(data, OFF_CGX),
84            ndblk: endian.u32(data, OFF_NDBLK),
85            niblk: endian.i32(data, OFF_NIBLK),
86            initediblk: endian.u32(data, OFF_INITEDIBLK),
87            iusedoff: endian.u32(data, OFF_IUSEDOFF),
88            freeoff: endian.u32(data, OFF_FREEOFF),
89            clusteroff: endian.u32(data, OFF_CLUSTEROFF),
90        })
91    }
92
93    /// Byte offset (from the header start) of the used-inode bitmap.
94    #[must_use]
95    pub fn inosused_off(&self) -> usize {
96        self.iusedoff as usize
97    }
98
99    /// Byte offset (from the header start) of the free-block bitmap.
100    #[must_use]
101    pub fn blksfree_off(&self) -> usize {
102        self.freeoff as usize
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn synthetic(endian: Endian) -> Vec<u8> {
111        let mut d = vec![0u8; CG_MIN_LEN];
112        let wr32 = |d: &mut [u8], off: usize, v: u32| {
113            let b = match endian {
114                Endian::Little => v.to_le_bytes(),
115                Endian::Big => v.to_be_bytes(),
116            };
117            d[off..off + 4].copy_from_slice(&b);
118        };
119        wr32(&mut d, OFF_MAGIC, CG_MAGIC);
120        wr32(&mut d, OFF_CGX, 0);
121        wr32(&mut d, OFF_NDBLK, 256);
122        wr32(&mut d, OFF_NIBLK, 8);
123        wr32(&mut d, OFF_INITEDIBLK, 128);
124        wr32(&mut d, OFF_IUSEDOFF, 168);
125        wr32(&mut d, OFF_FREEOFF, 184);
126        wr32(&mut d, OFF_CLUSTEROFF, 200);
127        d
128    }
129
130    #[test]
131    fn parses_valid_cg_little_endian() {
132        let d = synthetic(Endian::Little);
133        let cg = CylinderGroup::parse(&d, Endian::Little).unwrap();
134        assert_eq!(cg.cgx, 0);
135        assert_eq!(cg.ndblk, 256);
136        assert_eq!(cg.niblk, 8);
137        assert_eq!(cg.iusedoff, 168);
138        assert_eq!(cg.freeoff, 184);
139        assert_eq!(cg.inosused_off(), 168);
140        assert_eq!(cg.blksfree_off(), 184);
141    }
142
143    #[test]
144    fn parses_valid_cg_big_endian() {
145        let d = synthetic(Endian::Big);
146        let cg = CylinderGroup::parse(&d, Endian::Big).unwrap();
147        assert_eq!(cg.ndblk, 256);
148        assert_eq!(cg.iusedoff, 168);
149    }
150
151    #[test]
152    fn bad_cg_magic_fails_loud() {
153        let mut d = synthetic(Endian::Little);
154        d[OFF_MAGIC..OFF_MAGIC + 4].copy_from_slice(&0x1234_5678_u32.to_le_bytes());
155        let err = CylinderGroup::parse(&d, Endian::Little).unwrap_err();
156        assert!(
157            matches!(&err, UfsError::BadCgMagic { found, .. } if *found == 0x1234_5678),
158            "expected BadCgMagic with the offending value, got {err:?}"
159        );
160    }
161
162    #[test]
163    fn wrong_endian_is_detected_as_bad_magic() {
164        // A little-endian cg read as big-endian yields a byte-swapped magic.
165        let d = synthetic(Endian::Little);
166        assert!(matches!(
167            CylinderGroup::parse(&d, Endian::Big),
168            Err(UfsError::BadCgMagic { .. })
169        ));
170    }
171
172    #[test]
173    fn truncated_cg_does_not_panic() {
174        let d = vec![0u8; CG_MIN_LEN - 1];
175        assert!(matches!(
176            CylinderGroup::parse(&d, Endian::Little),
177            Err(UfsError::Truncated {
178                structure: "cylinder group",
179                ..
180            })
181        ));
182    }
183}