1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3
4pub const JOURNAL_MAGIC: u32 = 0xC03B_3998;
5
6pub const JBD2_FEATURE_INCOMPAT_64BIT: u32 = 0x0000_0002;
8pub const JBD2_FEATURE_INCOMPAT_CSUM_V2: u32 = 0x0000_0008;
9pub const JBD2_FEATURE_INCOMPAT_CSUM_V3: u32 = 0x0000_0010;
10
11fn check_len(buf: &[u8], need: usize, structure: &'static str) -> Result<()> {
19 if buf.len() < need {
20 Err(Ext4Error::TooShort {
21 structure,
22 expected: need,
23 found: buf.len(),
24 })
25 } else {
26 Ok(())
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum JournalBlockType {
34 Descriptor,
35 Commit,
36 SuperblockV1,
37 SuperblockV2,
38 Revoke,
39 Unknown(u32),
40}
41
42impl From<u32> for JournalBlockType {
43 fn from(v: u32) -> Self {
44 match v {
45 1 => Self::Descriptor,
46 2 => Self::Commit,
47 3 => Self::SuperblockV1,
48 4 => Self::SuperblockV2,
49 5 => Self::Revoke,
50 other => Self::Unknown(other),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
59pub struct JournalHeader {
60 pub magic: u32,
61 pub block_type: JournalBlockType,
62 pub sequence: u32,
63}
64
65impl JournalHeader {
66 pub fn parse(buf: &[u8]) -> Result<Self> {
67 check_len(buf, 12, "JournalHeader")?;
68 let magic = safe_read::be_u32(buf, 0);
69 if magic != JOURNAL_MAGIC {
70 return Err(Ext4Error::JournalCorrupt(format!(
71 "bad journal magic: 0x{magic:08X}"
72 )));
73 }
74 Ok(Self {
75 magic,
76 block_type: JournalBlockType::from(safe_read::be_u32(buf, 4)),
77 sequence: safe_read::be_u32(buf, 8),
78 })
79 }
80}
81
82#[derive(Debug, Clone)]
105pub struct JournalSuperblock {
106 pub header: JournalHeader,
107 pub block_size: u32,
108 pub max_len: u32,
109 pub first: u32,
110 pub sequence: u32,
111 pub start: u32,
112 pub errno: u32,
113 pub feature_compat: u32,
114 pub feature_incompat: u32,
115 pub feature_ro_compat: u32,
116 pub uuid: [u8; 16],
117 pub nr_users: u32,
118 pub checksum_type: u8,
119 pub checksum: u32,
120}
121
122impl JournalSuperblock {
123 pub fn parse(buf: &[u8]) -> Result<Self> {
124 check_len(buf, 0x100, "JournalSuperblock")?;
125 let header = JournalHeader::parse(buf)?;
126 let mut uuid = [0u8; 16];
127 uuid.copy_from_slice(&buf[0x30..0x40]);
128 Ok(Self {
129 header,
130 block_size: safe_read::be_u32(buf, 0x0C),
131 max_len: safe_read::be_u32(buf, 0x10),
132 first: safe_read::be_u32(buf, 0x14),
133 sequence: safe_read::be_u32(buf, 0x18),
134 start: safe_read::be_u32(buf, 0x1C),
135 errno: safe_read::be_u32(buf, 0x20),
136 feature_compat: safe_read::be_u32(buf, 0x24),
137 feature_incompat: safe_read::be_u32(buf, 0x28),
138 feature_ro_compat: safe_read::be_u32(buf, 0x2C),
139 uuid,
140 nr_users: safe_read::be_u32(buf, 0x40),
141 checksum_type: buf[0x50],
142 checksum: safe_read::be_u32(buf, 0xFC),
143 })
144 }
145
146 pub fn is_64bit(&self) -> bool {
147 self.feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0
148 }
149
150 pub fn has_csum_v3(&self) -> bool {
151 self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0
152 }
153
154 pub fn has_csum_v2(&self) -> bool {
155 self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V2 != 0
156 }
157}
158
159#[derive(Debug, Clone)]
170pub struct JournalBlockTag {
171 pub blocknr: u64,
172 pub checksum: u32,
173 pub escaped: bool,
174 pub same_uuid: bool,
175 pub last_tag: bool,
176 pub tag_size: usize,
178}
179
180impl JournalBlockTag {
181 pub fn parse_v3(buf: &[u8], is_64bit: bool) -> Result<Self> {
185 check_len(buf, 16, "JournalBlockTag")?;
186 let blocknr_lo = u64::from(safe_read::be_u32(buf, 0x00));
187 let flags = safe_read::be_u32(buf, 0x04);
188 let escaped = flags & 0x01 != 0;
189 let same_uuid = flags & 0x02 != 0;
190 let last_tag = flags & 0x08 != 0;
191 let blocknr_hi = if is_64bit {
192 u64::from(safe_read::be_u32(buf, 0x08))
193 } else {
194 0
195 };
196 let blocknr = (blocknr_hi << 32) | blocknr_lo;
197 let checksum = safe_read::be_u32(buf, 0x0C);
198 let tag_size = if same_uuid { 16 } else { 32 };
200 Ok(Self {
201 blocknr,
202 checksum,
203 escaped,
204 same_uuid,
205 last_tag,
206 tag_size,
207 })
208 }
209}
210
211#[derive(Debug, Clone)]
221pub struct JournalCommit {
222 pub sequence: u32,
223 pub commit_seconds: i64,
224 pub commit_nanoseconds: u32,
225}
226
227impl JournalCommit {
228 pub fn parse(buf: &[u8]) -> Result<Self> {
229 check_len(buf, 0x3C, "JournalCommit")?;
230 let header = JournalHeader::parse(buf)?;
231 Ok(Self {
232 sequence: header.sequence,
233 commit_seconds: safe_read::be_u64(buf, 0x30) as i64,
234 commit_nanoseconds: safe_read::be_u32(buf, 0x38),
235 })
236 }
237}
238
239#[derive(Debug, Clone)]
248pub struct JournalRevoke {
249 pub sequence: u32,
250 pub revoked_blocks: Vec<u64>,
251}
252
253impl JournalRevoke {
254 pub fn parse(buf: &[u8], is_64bit: bool) -> Result<Self> {
255 check_len(buf, 16, "JournalRevoke")?;
256 let header = JournalHeader::parse(buf)?;
257 let byte_count = safe_read::be_u32(buf, 12) as usize;
258 check_len(buf, byte_count, "JournalRevoke data")?;
259 let entry_size = if is_64bit { 8usize } else { 4 };
260 let data = &buf[16..byte_count];
261 let mut revoked_blocks = Vec::with_capacity(data.len() / entry_size);
262 let mut off = 0;
263 while off + entry_size <= data.len() {
264 let blk = if is_64bit {
265 safe_read::be_u64(data, off)
266 } else {
267 u64::from(safe_read::be_u32(data, off))
268 };
269 revoked_blocks.push(blk);
270 off += entry_size;
271 }
272 Ok(Self {
273 sequence: header.sequence,
274 revoked_blocks,
275 })
276 }
277}
278
279#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn parse_journal_header() {
287 let mut buf = vec![0u8; 12];
288 buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
289 buf[4..8].copy_from_slice(&1u32.to_be_bytes()); buf[8..12].copy_from_slice(&42u32.to_be_bytes());
291 let hdr = JournalHeader::parse(&buf).unwrap();
292 assert_eq!(hdr.magic, JOURNAL_MAGIC);
293 assert_eq!(hdr.block_type, JournalBlockType::Descriptor);
294 assert_eq!(hdr.sequence, 42);
295 }
296
297 #[test]
298 fn reject_bad_journal_magic() {
299 let buf = vec![0u8; 12];
300 let err = JournalHeader::parse(&buf).unwrap_err();
301 assert!(matches!(err, crate::error::Ext4Error::JournalCorrupt(_)));
302 }
303
304 #[test]
305 fn parse_journal_superblock() {
306 let mut buf = vec![0u8; 1024];
307 buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
308 buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes());
310 buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); buf[0x10..0x14].copy_from_slice(&1024u32.to_be_bytes()); buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); let jsb = JournalSuperblock::parse(&buf).unwrap();
314 assert_eq!(jsb.block_size, 4096);
315 assert_eq!(jsb.max_len, 1024);
316 assert_eq!(jsb.first, 1);
317 }
318
319 #[test]
320 fn parse_v3_rejects_truncated_tag() {
321 let short = [0u8; 8];
324 assert!(JournalBlockTag::parse_v3(&short, false).is_err());
325 }
326
327 #[test]
328 fn parse_block_tag_v3() {
329 let mut buf = vec![0u8; 16];
330 buf[0..4].copy_from_slice(&500u32.to_be_bytes()); buf[4..8].copy_from_slice(&0x08u32.to_be_bytes()); buf[12..16].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
333 let tag = JournalBlockTag::parse_v3(&buf, false).unwrap();
334 assert_eq!(tag.blocknr, 500);
335 assert!(tag.last_tag);
336 assert!(!tag.escaped);
337 }
338
339 #[test]
340 fn journal_superblock_feature_flags() {
341 let mut buf = vec![0u8; 1024];
342 buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
343 buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes()); buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes()); buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes()); buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); let features: u32 = JBD2_FEATURE_INCOMPAT_64BIT | JBD2_FEATURE_INCOMPAT_CSUM_V3;
350 buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
351
352 let jsb = JournalSuperblock::parse(&buf).unwrap();
353 assert!(jsb.is_64bit());
354 assert!(jsb.has_csum_v3());
355 assert!(!jsb.has_csum_v2());
357 }
358
359 #[test]
360 fn journal_superblock_csum_v2_only() {
361 let mut buf = vec![0u8; 1024];
362 buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
363 buf[4..8].copy_from_slice(&4u32.to_be_bytes()); buf[8..12].copy_from_slice(&1u32.to_be_bytes());
365 buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes());
366 buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes());
367 buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes());
368 let features: u32 = JBD2_FEATURE_INCOMPAT_CSUM_V2; buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
370
371 let jsb = JournalSuperblock::parse(&buf).unwrap();
372 assert!(!jsb.is_64bit());
373 assert!(!jsb.has_csum_v3());
374 assert!(jsb.has_csum_v2());
375 }
376
377 #[test]
378 fn parse_commit_block() {
379 let mut buf = vec![0u8; 64];
380 buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
381 buf[4..8].copy_from_slice(&2u32.to_be_bytes()); buf[8..12].copy_from_slice(&99u32.to_be_bytes());
383 buf[0x30..0x38].copy_from_slice(&1_700_000_000u64.to_be_bytes());
384 buf[0x38..0x3C].copy_from_slice(&500_000_000u32.to_be_bytes());
385 let commit = JournalCommit::parse(&buf).unwrap();
386 assert_eq!(commit.sequence, 99);
387 assert_eq!(commit.commit_seconds, 1_700_000_000);
388 assert_eq!(commit.commit_nanoseconds, 500_000_000);
389 }
390}