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