Skip to main content

f2fs_reader/
reader.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use crate::block_cache::BlockCache;
5use crate::checkpoint::*;
6use crate::crypto;
7use crate::dir::{DentryBlock, DirEntry};
8use crate::inode::{self, Inode};
9use crate::nat::{Nat, NatJournal, RawNatEntry, SummaryBlock};
10use crate::superblock::{
11    BLOCK_SIZE, BLOCKS_PER_SEGMENT, F2FS_MAGIC, SEGMENT_SIZE, SUPERBLOCK_OFFSET, SuperBlock,
12    f2fs_crc32,
13};
14use anyhow::{Error, anyhow, bail, ensure};
15use async_trait::async_trait;
16use std::collections::HashMap;
17use std::sync::Arc;
18use storage_device::Device;
19use storage_device::buffer::Buffer;
20use zerocopy::FromBytes;
21
22// Used to indicate zero pages (when used as block_addr) and end of list (when used as nid).
23pub const NULL_ADDR: u32 = 0;
24// Used to indicate a new page that hasn't been allocated yet.
25pub const NEW_ADDR: u32 = 0xffffffff;
26
27/// This trait is exposed to allow unit testing of Inode and other structs.
28/// It is implemented by F2fsReader.
29#[async_trait]
30pub(super) trait Reader {
31    /// Read a raw block from disk.
32    /// `block_addr` is the physical block offset on the device.
33    async fn read_raw_block(&self, block_addr: u32) -> Result<Buffer<'_>, Error>;
34
35    /// Reads a logical 'node' block from the disk (i.e. via NAT indirection)
36    async fn read_node(&self, nid: u32) -> Result<Buffer<'_>, Error>;
37
38    /// Attempt to retrieve a key given its identifier.
39    fn get_key(&self, _identifier: &[u8; 16]) -> Option<&[u8; 64]> {
40        None
41    }
42
43    /// Returns the filesystem UUID. This is needed for some decryption policies.
44    fn fs_uuid(&self) -> &[u8; 16];
45
46    /// Attempt to obtain a decryptor for a given crypto context.
47    /// Will return None if the main key is not known.
48    fn get_decryptor_for_inode(&self, inode: &Inode) -> Option<crypto::PerFileDecryptor> {
49        if let Some(context) = inode.context {
50            if let Some(main_key) = self.get_key(&context.main_key_identifier) {
51                return Some(crypto::PerFileDecryptor::new(main_key, context, self.fs_uuid()));
52            }
53        }
54        None
55    }
56
57    /// Look up a raw NAT entry given a node ID.
58    async fn get_nat_entry(&self, nid: u32) -> Result<RawNatEntry, Error>;
59}
60
61pub struct F2fsReader {
62    device: Arc<dyn Device>,
63    superblock: SuperBlock,     // 1kb, points at checkpoints
64    checkpoint: CheckpointPack, // pair of a/b segments (alternating versions)
65    cp_start_block: u32,        // Start block of the active checkpoint
66    nat: Nat,
67
68    // A simple key store.
69    keys: HashMap<[u8; 16], [u8; 64]>,
70    cache: BlockCache,
71}
72
73impl Drop for F2fsReader {
74    fn drop(&mut self) {
75        // Zero keys in RAM for extra safety.
76        self.keys.values_mut().for_each(|v| {
77            *v = [0u8; 64];
78        });
79    }
80}
81
82impl F2fsReader {
83    pub fn superblock(&self) -> &SuperBlock {
84        &self.superblock
85    }
86
87    pub fn checkpoint(&self) -> &CheckpointPack {
88        &self.checkpoint
89    }
90
91    pub async fn open_device(device: Arc<dyn Device>) -> Result<Self, Error> {
92        let (superblock, checkpoints) =
93            match Self::try_from_superblock(device.as_ref(), SUPERBLOCK_OFFSET).await {
94                Ok(x) => x,
95                Err(e) => Self::try_from_superblock(device.as_ref(), SUPERBLOCK_OFFSET * 2)
96                    .await
97                    .map_err(|_| e)?,
98            };
99
100        let mut last_error = anyhow!("No checkpoints found");
101
102        for (checkpoint, cp_start_block) in checkpoints {
103            let mut this = Self {
104                device: device.clone(),
105                superblock,
106                checkpoint,
107                cp_start_block,
108                nat: Nat::new(0, vec![], HashMap::new()),
109                keys: HashMap::with_capacity(16),
110                cache: BlockCache::new(1024, BLOCK_SIZE),
111            };
112
113            match this.read_nat_journal().await {
114                Ok(nat_journal) => {
115                    this.nat = Nat::new(
116                        this.superblock.nat_blkaddr,
117                        this.checkpoint.nat_bitmap.clone(),
118                        nat_journal,
119                    );
120                    return Ok(this);
121                }
122                Err(e) => {
123                    let ver = this.checkpoint.header.checkpoint_ver;
124                    log::warn!(
125                        "Failed to initialize from checkpoint (Ver {} at {}): {}. Trying next.",
126                        ver,
127                        cp_start_block,
128                        e
129                    );
130                    last_error = e;
131                    // Continue loop to try next checkpoint
132                }
133            }
134        }
135
136        Err(last_error)
137    }
138
139    async fn try_from_superblock(
140        device: &dyn Device,
141        superblock_offset: u64,
142    ) -> Result<(SuperBlock, Vec<(CheckpointPack, u32)>), Error> {
143        let superblock = SuperBlock::read_from_device(device, superblock_offset).await?;
144        let checkpoint_addr = superblock.cp_blkaddr;
145        let checkpoint_a_offset = BLOCK_SIZE as u64 * checkpoint_addr as u64;
146        let checkpoint_b_offset = checkpoint_a_offset + SEGMENT_SIZE as u64;
147
148        let mut checkpoints = Vec::new();
149
150        // Read both checkpoints and collect valid ones with their block addresses
151        if let Ok(cp) = CheckpointPack::read_from_device(device, checkpoint_a_offset).await {
152            checkpoints.push((cp, checkpoint_addr));
153        }
154        if let Ok(cp) = CheckpointPack::read_from_device(device, checkpoint_b_offset).await {
155            checkpoints.push((cp, checkpoint_addr + BLOCKS_PER_SEGMENT as u32));
156        }
157
158        if checkpoints.is_empty() {
159            bail!("Failed to read any valid checkpoint");
160        }
161
162        // Sort by version descending (newest first)
163        checkpoints.sort_by(|(a, _), (b, _)| {
164            let va = a.header.checkpoint_ver;
165            let vb = b.header.checkpoint_ver;
166            vb.cmp(&va)
167        });
168
169        // Min metadata segment count is 1 superblock, 1 ssa, (ckpt + sit + nat) * 2
170        const MIN_METADATA_SEGMENT_COUNT: u32 = 8;
171
172        // Use newest for validation
173        let first_cp = &checkpoints[0].0;
174
175        // Make sure the metadata fits on the device
176        let metadata_segment_count = superblock
177            .segment_count_sit
178            .checked_add(superblock.segment_count_nat)
179            .and_then(|v| v.checked_add(first_cp.header.rsvd_segment_count))
180            .and_then(|v| v.checked_add(superblock.segment_count_ssa))
181            .and_then(|v| v.checked_add(superblock.segment_count_ckpt))
182            .ok_or_else(|| anyhow::anyhow!("Segment counts overflow"))?;
183        ensure!(
184            metadata_segment_count <= superblock.segment_count
185                && metadata_segment_count >= MIN_METADATA_SEGMENT_COUNT,
186            "Bad segment counts in checkpoint"
187        );
188        Ok((superblock, checkpoints))
189    }
190
191    /// Returns the block address that the checkpoint starts at.
192    pub fn checkpoint_start_addr(&self) -> u32 {
193        self.cp_start_block
194    }
195
196    fn nat(&self) -> &Nat {
197        &self.nat
198    }
199    /// Returns the absolute block address of the summary block (default or compact).
200    /// handles CP_ORPHAN_PRESENT_FLAG for compact summaries.
201    pub fn summary_block_addr(&self) -> u32 {
202        let mut offset = self.checkpoint.header.cp_pack_start_sum;
203        if self.checkpoint.header.ckpt_flags & CP_ORPHAN_PRESENT_FLAG != 0 {
204            // If orphans are present, they occupy the block at `cp_pack_start_sum`.
205            // The actual summary block follows it.
206            offset += 1;
207        }
208        self.checkpoint_start_addr() + offset
209    }
210
211    async fn read_nat_journal(&mut self) -> Result<HashMap<u32, RawNatEntry>, Error> {
212        if self.checkpoint.header.ckpt_flags & CKPT_FLAG_COMPACT_SUMMARY != 0 {
213            // The "compact summary" feature packs NAT/SIT/summary into one block.
214            // The NAT journal entries come first.
215            let summary_addr = self.summary_block_addr();
216            let block = self.read_raw_block(summary_addr).await?;
217            let n_nats = block.as_ptr_slice().read::<u16>().unwrap();
218            let nat_journal = block
219                .as_ptr_slice()
220                .subslice(2..2 + std::mem::size_of::<NatJournal>())
221                .read::<NatJournal>()
222                .unwrap();
223            ensure!(
224                (n_nats as usize) <= nat_journal.entries.len(),
225                "n_nats {} larger than block size {}",
226                n_nats,
227                nat_journal.entries.len()
228            );
229            Ok(HashMap::from_iter(
230                nat_journal.entries[..n_nats as usize].iter().map(|e| (e.ino, e.entry)),
231            ))
232        } else {
233            // Read the default summary block location from the "hot data" segment.
234            // If orphans are present, `summary_block_addr` automatically skips the orphan block.
235            let summary_addr = self.summary_block_addr();
236            let block = self.read_raw_block(summary_addr).await?;
237
238            let summary = block
239                .as_ptr_slice()
240                .read::<SummaryBlock>()
241                .ok_or_else(|| anyhow!("Block size too small"))?;
242            ensure!(summary.footer.entry_type == 0u8, "sum_type != 0 in summary footer");
243            #[cfg(not(fuzz))]
244            {
245                let data = block.to_vec();
246                let actual_checksum = f2fs_crc32(F2FS_MAGIC, &data[..BLOCK_SIZE - 4]);
247                let expected_checksum = summary.footer.check_sum;
248                if actual_checksum != expected_checksum {
249                    // TODO(b/487023899): Confirm semantics.
250                    log::warn!(
251                        "Summary block checksum mismatch (actual: 0x{:x}, expected: 0x{:x}). \
252                     This is normal for checkpoints with CP_CRC_RECOVERY_FLAG.",
253                        actual_checksum,
254                        expected_checksum
255                    );
256                }
257            }
258            let n_nats = summary.n_nats;
259            ensure!(
260                (n_nats as usize) <= summary.nat_journal.entries.len(),
261                "n_nats {} larger than block size {}",
262                n_nats,
263                summary.nat_journal.entries.len()
264            );
265            let mut out = HashMap::new();
266            for i in 0..n_nats as usize {
267                out.insert(
268                    summary.nat_journal.entries[i].ino,
269                    summary.nat_journal.entries[i].entry,
270                );
271            }
272            Ok(out)
273        }
274    }
275
276    pub fn root_ino(&self) -> u32 {
277        self.superblock.root_ino
278    }
279
280    /// Gives the maximum addressable inode. This can be used to ensure we don't have namespace
281    /// collisions when building hybrid images.
282    pub fn max_ino(&self) -> u32 {
283        (self.checkpoint.nat_bitmap.len() * 8) as u32
284    }
285
286    /// Registers a new main key.
287    /// This 'unlocks' any files using this key.
288    pub fn add_key(&mut self, main_key: &[u8; 64]) -> [u8; 16] {
289        let identifier = fscrypt::main_key_to_identifier(main_key);
290        println!("Adding key with identifier {}", hex::encode(identifier));
291        self.keys.insert(identifier.clone(), main_key.clone());
292        identifier
293    }
294
295    /// Read an inode for a directory and return entries.
296    pub async fn readdir(&self, ino: u32) -> Result<Vec<DirEntry>, Error> {
297        let inode = Inode::try_load(self, ino).await?;
298        let decryptor = self.get_decryptor_for_inode(&inode);
299        let mode = inode.header.mode;
300        let advise_flags = inode.header.advise_flags;
301        let flags = inode.header.flags;
302        ensure!(mode.contains(inode::Mode::Directory), "not a directory");
303        if let Some(entries) = inode.get_inline_dir_entries(
304            advise_flags.contains(inode::AdviseFlags::Encrypted),
305            flags.contains(inode::Flags::Casefold),
306            &decryptor,
307        )? {
308            Ok(entries)
309        } else {
310            let mut entries = Vec::new();
311
312            // Entries are stored in a series of increasingly larger hash tables.
313            // The number of these that exist are based on inode.dir_depth.
314            // Thankfully, we don't need to worry about this as the total number of blocks is
315            // bound to inode.header.size and we can just skip NULL blocks.
316            for mut extent in inode.data_blocks() {
317                for _ in 0..extent.length {
318                    let dentry_block = self
319                        .read_raw_block(extent.physical_block_num)
320                        .await?
321                        .as_ptr_slice()
322                        .read::<DentryBlock>()
323                        .ok_or_else(|| anyhow!("Block size too small"))?;
324                    entries.append(&mut dentry_block.get_entries(
325                        ino,
326                        advise_flags.contains(inode::AdviseFlags::Encrypted),
327                        flags.contains(inode::Flags::Casefold),
328                        &decryptor,
329                    )?);
330                    extent.physical_block_num += 1;
331                }
332            }
333            Ok(entries)
334        }
335    }
336
337    /// Read an inode and associated blocks from disk.
338    pub async fn read_inode(&self, ino: u32) -> Result<Box<Inode>, Error> {
339        Inode::try_load(self, ino).await
340    }
341
342    /// Takes an inode for a symlink and the link as a set of bytes, decrypted if possible.
343    pub fn read_symlink(&self, inode: &Inode) -> Result<Box<[u8]>, Error> {
344        if let Some(inline_data) = inode.inline_data.as_deref() {
345            let mut filename = inline_data.to_vec();
346            if inode.header.advise_flags.contains(inode::AdviseFlags::Encrypted) {
347                // Encrypted symlinks have a 2-byte length prefix.
348                ensure!(filename.len() >= 2, "invalid encrypted symlink");
349                let symlink_len = u16::read_from_bytes(&filename[..2]).unwrap();
350                filename.drain(..2);
351                filename.truncate(symlink_len as usize);
352                ensure!(symlink_len == filename.len() as u16, "invalid encrypted symlink");
353                if let Some(decryptor) = self.get_decryptor_for_inode(inode) {
354                    decryptor.decrypt_filename_data(inode.footer.ino, &mut filename);
355                } else {
356                    // Symlinks don't have a hash code, so we just use 0.
357                    let proxy_filename: String =
358                        fscrypt::proxy_filename::ProxyFilename::new_with_hash_code(0, &filename)
359                            .into();
360                    filename = proxy_filename.as_bytes().to_vec();
361                }
362                // Unfortunately, it seems we still have to remove trailing nulls.
363                // fscrypt + f2fs publishes a file size equal to padded symlink length + 2 bytes.
364                while let Some(0) = filename.last() {
365                    filename.pop();
366                }
367            }
368            Ok(filename.into_boxed_slice())
369        } else {
370            bail!("Not a valid symlink");
371        }
372    }
373
374    /// Reads and returns a data block of a file.
375    /// On success, this will return Some(Buffer) containing the data or None if the file is sparse.
376    pub async fn read_data(&self, inode: &Inode, block_num: u32) -> Result<Option<Vec<u8>>, Error> {
377        let inline_flags = inode.header.inline_flags;
378        ensure!(
379            !inline_flags.contains(crate::InlineFlags::Data),
380            "Can't use read_data() on inline file."
381        );
382        let block_addr = inode.data_block_addr(block_num);
383        if block_addr == NULL_ADDR || block_addr == NEW_ADDR {
384            // Treat as an empty page
385            return Ok(None);
386        }
387        let buffer = self.read_raw_block(block_addr).await?;
388        let mut data = buffer.to_vec();
389        if let Some(decryptor) = self.get_decryptor_for_inode(inode) {
390            decryptor.decrypt_data(inode.footer.ino, block_num, &mut data);
391        }
392        Ok(Some(data))
393    }
394}
395
396#[async_trait]
397impl Reader for F2fsReader {
398    /// `block_addr` is the physical block offset on the device.
399    async fn read_raw_block(&self, block_addr: u32) -> Result<Buffer<'_>, Error> {
400        if let Some(block) = self.cache.get_buffer(block_addr, self.device.as_ref()).await {
401            return Ok(block);
402        }
403
404        const READAHEAD: u64 = 16;
405        let end = std::cmp::min(block_addr as u64 + READAHEAD, self.device.block_count());
406        let count = end.saturating_sub(block_addr as u64).max(1) as usize;
407
408        let mut buffer = self.device.allocate_buffer(count * BLOCK_SIZE).await;
409        self.device
410            .read(block_addr as u64 * BLOCK_SIZE as u64, buffer.as_mut())
411            .await
412            .map_err(|_| anyhow!("device read failed"))?;
413
414        for i in 0..count {
415            let subslice = buffer.as_ptr_slice().subslice(i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE);
416            self.cache.insert(block_addr + i as u32, subslice.to_vec());
417        }
418        Ok(self.cache.get_buffer(block_addr, self.device.as_ref()).await.unwrap())
419    }
420
421    async fn read_node(&self, nid: u32) -> Result<Buffer<'_>, Error> {
422        let nat_entry = self.get_nat_entry(nid).await?;
423        self.read_raw_block(nat_entry.block_addr).await
424    }
425
426    fn get_key(&self, identifier: &[u8; 16]) -> Option<&[u8; 64]> {
427        self.keys.get(identifier)
428    }
429
430    fn fs_uuid(&self) -> &[u8; 16] {
431        &self.superblock.uuid
432    }
433
434    async fn get_nat_entry(&self, nid: u32) -> Result<RawNatEntry, Error> {
435        if let Some(entry) = self.nat().nat_journal.get(&nid) {
436            return Ok(*entry);
437        }
438        let nat_block_addr = self.nat().get_nat_block_for_entry(nid)?;
439        let offset = self.nat().get_nat_block_offset_for_entry(nid);
440        let block = self.read_raw_block(nat_block_addr).await?;
441        let entry = block
442            .as_ptr_slice()
443            .subslice(offset..offset + std::mem::size_of::<RawNatEntry>())
444            .read::<RawNatEntry>()
445            .ok_or_else(|| anyhow!("Block size too small"))?;
446        Ok(entry)
447    }
448}
449
450#[cfg(test)]
451mod test {
452    use super::*;
453    use crate::dir::FileType;
454    use crate::xattr;
455    use std::collections::HashSet;
456    use std::path::PathBuf;
457    use std::sync::Arc;
458
459    use storage_device::fake_device::FakeDevice;
460
461    fn open_test_image(path: &str) -> FakeDevice {
462        let path = std::path::PathBuf::from(path);
463        println!("path is {path:?}");
464        FakeDevice::from_image(
465            zstd::Decoder::new(std::fs::File::open(&path).expect("open image"))
466                .expect("decompress image"),
467            BLOCK_SIZE as u32,
468        )
469        .expect("open image")
470    }
471
472    #[fuchsia::test]
473    async fn test_open_fs() {
474        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
475
476        let f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
477        // Root inode is a known constant.
478        assert_eq!(f2fs.root_ino(), 3);
479        let superblock = &f2fs.superblock;
480        let major_ver = superblock.major_ver;
481        let minor_ver = superblock.minor_ver;
482        assert_eq!(major_ver, 1);
483        assert_eq!(minor_ver, 16);
484        assert_eq!(superblock.get_total_size(), 256 << 20);
485        assert_eq!(superblock.get_volume_name().expect("get volume name"), "testimage");
486    }
487
488    // Helper method to walk paths.
489    async fn resolve_inode_path(f2fs: &F2fsReader, path: &str) -> Result<u32, Error> {
490        let path = PathBuf::from(path.strip_prefix("/").unwrap());
491        let mut ino = f2fs.root_ino();
492        for filename in &path {
493            let entries = f2fs.readdir(ino).await?;
494            if let Some(entry) = entries.iter().filter(|e| *e.filename == *filename).next() {
495                ino = entry.ino;
496            } else {
497                bail!("Not found.");
498            }
499        }
500        Ok(ino)
501    }
502
503    #[fuchsia::test]
504    async fn test_basic_dirs() {
505        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
506
507        let f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
508        let root_ino = f2fs.root_ino();
509        let root_entries = f2fs.readdir(root_ino).await.expect("readdir");
510        assert_eq!(root_entries.len(), 7);
511        assert_eq!(root_entries[0].filename, "a");
512        assert_eq!(root_entries[0].file_type, FileType::Directory);
513        assert_eq!(root_entries[1].filename, "large_dir");
514        assert_eq!(root_entries[2].filename, "large_dir2");
515        assert_eq!(root_entries[3].filename, "sparse.dat");
516        assert_eq!(root_entries[4].filename, "verity");
517        assert_eq!(root_entries[5].filename, "fscrypt");
518        assert_eq!(root_entries[6].filename, "large_zero");
519
520        let inlined_file_ino =
521            resolve_inode_path(&f2fs, "/a/b/c/inlined").await.expect("resolve inlined");
522        let inode = Inode::try_load(&f2fs, inlined_file_ino).await.expect("load inode");
523        let block_size = inode.header.block_size;
524        let size = inode.header.size;
525        assert_eq!(block_size, 1);
526        assert_eq!(size, 12);
527        assert_eq!(inode.inline_data.unwrap().as_ref(), "inline_data\n".as_bytes());
528
529        const REG_FILE_SIZE: u64 = 8 * BLOCK_SIZE as u64 + 8;
530        const REG_FILE_BLOCKS: u64 = 9 + 1;
531        let regular_file_ino =
532            resolve_inode_path(&f2fs, "/a/b/c/regular").await.expect("resolve regular");
533        let inode = Inode::try_load(&f2fs, regular_file_ino).await.expect("load inode");
534        let block_size = inode.header.block_size;
535        let size = inode.header.size;
536        assert_eq!(block_size, REG_FILE_BLOCKS);
537        assert_eq!(size, REG_FILE_SIZE);
538        assert!(inode.inline_data.is_none());
539        for i in 0..8 {
540            assert_eq!(
541                f2fs.read_data(&inode, i).await.expect("read data").unwrap(),
542                vec![0u8; BLOCK_SIZE]
543            );
544        }
545        assert_eq!(
546            &f2fs.read_data(&inode, 8).await.expect("read data").unwrap()[..9],
547            b"01234567\0"
548        );
549
550        let symlink_ino =
551            resolve_inode_path(&f2fs, "/a/b/c/symlink").await.expect("resolve symlink");
552        let inode = Inode::try_load(&f2fs, symlink_ino).await.expect("load inode");
553        assert_eq!(f2fs.read_symlink(&inode).expect("read_symlink").as_ref(), b"regular");
554
555        let hardlink_ino =
556            resolve_inode_path(&f2fs, "/a/b/c/hardlink").await.expect("resolve hardlink");
557        let inode = Inode::try_load(&f2fs, hardlink_ino).await.expect("load inode");
558        let block_size = inode.header.block_size;
559        let size = inode.header.size;
560        assert_eq!(block_size, REG_FILE_BLOCKS);
561        assert_eq!(size, REG_FILE_SIZE);
562
563        let chowned_ino =
564            resolve_inode_path(&f2fs, "/a/b/c/chowned").await.expect("resolve chowned");
565        let inode = Inode::try_load(&f2fs, chowned_ino).await.expect("load inode");
566        let uid = inode.header.uid;
567        let gid = inode.header.gid;
568        assert_eq!(uid, 999);
569        assert_eq!(gid, 999);
570
571        let large_dir = resolve_inode_path(&f2fs, "/large_dir").await.expect("resolve large_dir");
572        assert_eq!(f2fs.readdir(large_dir).await.expect("readdir").len(), 2001);
573
574        let large_dir2 = resolve_inode_path(&f2fs, "/large_dir2").await.expect("resolve large_dir");
575        assert_eq!(f2fs.readdir(large_dir2).await.expect("readdir").len(), 1);
576
577        let sparse_dat =
578            resolve_inode_path(&f2fs, "/sparse.dat").await.expect("resolve sparse.dat");
579        let inode = Inode::try_load(&f2fs, sparse_dat).await.expect("load inode");
580        let data_blocks: Vec<_> = inode.data_blocks().into_iter().collect();
581        assert_eq!(data_blocks.len(), 6);
582        assert_eq!(data_blocks[0].logical_block_num, 0);
583        assert_eq!(data_blocks[0].length, 1);
584        // Raw read of block.
585        let block =
586            f2fs.read_raw_block(data_blocks[0].physical_block_num).await.expect("read sparse");
587        assert_eq!(&block.to_vec()[..3], b"foo");
588        // The following chain of blocks are designed to land in each of the self.nids[] ranges.
589        assert_eq!(data_blocks[1].logical_block_num, 923);
590        assert_eq!(data_blocks[1].length, 1);
591        assert_eq!(data_blocks[2].logical_block_num, 1941);
592        assert_eq!(data_blocks[2].length, 1);
593        assert_eq!(data_blocks[3].logical_block_num, 2959);
594        assert_eq!(data_blocks[3].length, 1);
595        assert_eq!(data_blocks[4].logical_block_num, 1039283);
596        assert_eq!(data_blocks[4].length, 1);
597        assert_eq!(data_blocks[5].logical_block_num, 104671683);
598        assert_eq!(data_blocks[5].length, 2);
599        let block =
600            f2fs.read_raw_block(data_blocks[5].physical_block_num).await.expect("read sparse");
601        assert_eq!(block.to_vec(), vec![0; BLOCK_SIZE]);
602        // Exercise helper method to read block.
603        assert_eq!(
604            &f2fs.read_data(&inode, 104671684).await.expect("read data block").unwrap()[..3],
605            b"bar"
606        );
607        // Exercise helper method on zero page. Expect to get back 'None'.
608        assert!(f2fs.read_data(&inode, 104671684 - 10).await.expect("read data block").is_none());
609    }
610
611    #[fuchsia::test]
612    async fn test_xattr() {
613        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
614
615        let f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
616        let sparse_dat =
617            resolve_inode_path(&f2fs, "/sparse.dat").await.expect("resolve sparse.dat");
618        let inode = Inode::try_load(&f2fs, sparse_dat).await.expect("load inode");
619        assert_eq!(
620            inode.xattr,
621            vec![
622                xattr::XattrEntry {
623                    index: xattr::Index::User,
624                    name: Box::new(b"a".to_owned()),
625                    value: Box::new(b"value".to_owned())
626                },
627                xattr::XattrEntry {
628                    index: xattr::Index::User,
629                    name: Box::new(b"c".to_owned()),
630                    value: Box::new(b"value".to_owned())
631                },
632                xattr::XattrEntry {
633                    index: xattr::Index::User,
634                    name: Box::new(b"padding_test_1".to_owned()),
635                    value: Box::new(b"v".to_owned())
636                },
637                xattr::XattrEntry {
638                    index: xattr::Index::User,
639                    name: Box::new(b"padding_test_2".to_owned()),
640                    value: Box::new(b"va".to_owned())
641                },
642                xattr::XattrEntry {
643                    index: xattr::Index::User,
644                    name: Box::new(b"padding_test_3".to_owned()),
645                    value: Box::new(b"val".to_owned())
646                },
647                xattr::XattrEntry {
648                    index: xattr::Index::User,
649                    name: Box::new(b"padding_test_4".to_owned()),
650                    value: Box::new(b"valu".to_owned())
651                },
652                xattr::XattrEntry {
653                    index: xattr::Index::User,
654                    name: Box::new(b"padding_test_5".to_owned()),
655                    value: Box::new(b"value".to_owned())
656                },
657            ]
658        );
659    }
660
661    #[fuchsia::test]
662    async fn test_fsverity() {
663        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
664        let mut f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
665        f2fs.add_key(&[0u8; 64]);
666        let verity_files = vec![
667            "/verity/inlined",
668            "/verity/regular",
669            "/verity/merkle_layers.dat",
670            "/fscrypt/a/b/regular",
671        ];
672        for file_path in verity_files {
673            let file = resolve_inode_path(&f2fs, file_path).await.expect("resolve file");
674            let inode = Inode::try_load(&f2fs, file).await.expect("load inode");
675            assert!(inode.header.advise_flags.contains(inode::AdviseFlags::Verity));
676        }
677        // Verify other files aren't marked for verity.
678        let file = resolve_inode_path(&f2fs, "/a/b/c/regular").await.expect("resolve file");
679        let inode = Inode::try_load(&f2fs, file).await.expect("load inode");
680        assert!(!inode.header.advise_flags.contains(inode::AdviseFlags::Verity));
681        // TODO(https://fxbug.dev/399727919): Handle the verity descriptor and merkle tree parsing.
682    }
683
684    #[fuchsia::test]
685    async fn test_fbe() {
686        // Note: The synthetic filenames below are based on the nonce generated at file/directory
687        // creation time. This will differ each time a new image is generated.
688        // They can be extracted with a simple 'ls -l' by mounting the generated image. i.e.
689        //   $ zstd -d testdata/f2fs.img.st
690        //   $ sudo mount testdata/f2fs.img /mnt
691        //   $ ls /mnt/fscrypt -lR
692
693        // /fscrypt/<a>/<b>/<symlink>
694        let str_a = "2ll82QAAAADywluz1Ule7OVNBxUfa5Mw";
695        let str_b = "sttckQAAAADLBOCVVgjrZ-CXNkj5E6Cr";
696        let str_symlink = "zHAtQgAAAACRNPQYvCKuQo5F8rQUORg3";
697        let bytes_symlink_content = b"AAAAAAAAAADUsYZ_qNiiouF7e40xm65S";
698
699        let mut expected : HashSet<_> = [ // files in fscrypt/ dir.
700            "2ll82QAAAADywluz1Ule7OVNBxUfa5Mw",
701            "65OSUQAAAADqOiZJcQ1El2dpVdYMy84l",
702            "7vcnbgAAAAAOWdQfi4wK46uRGQBD0YSy",
703            "9Gsv9QAAAADjTeJ_9WdCxZMVTiSWhsWR",
704            "FAqGXAAAAAD1jOLXaZN-o8X9PoS67GI7",
705            "Rq5qZAAAAAA3y2lvAqesYDnVJWMklWnj",
706            "S93sdgAAAABo-YmXNPKtv4wxQCcUslTu",
707            "VP8QBwAAAAATw6Ozex0N2gMYrnDsB2aH",
708            "xUNjwgAAAADB0pEx5ovwx-AS02L0d1j7VMBRXzM4YnBri2pbasOqbFLhtegXr9kDGNcYd_hyk2mOkQIqu8hk7eARlFl-bq1yLhikhIT9HVC3FMrI7vQ-ewncEjXLDP3KK6RtH3r34S89AlzJZ4DVfXrr_Q5N5mANBbGTzeO70aJHL0Ms-MgkKwjHcbIxXLwcjE2B-mssLAvXam58pSD-aazxS_J2hrxOHGoUYiVJ-rXHozmKxBdWAO6OUW65",
709        ].into_iter().collect();
710
711        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
712
713        let mut f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
714
715        // First without the key...
716        // (The filenames below have been extracted from the generated image by
717        // mounting it and manually inspecting.)
718        resolve_inode_path(&f2fs, "/fscrypt/a/b/regular")
719            .await
720            .expect_err("resolve fscrypt regular");
721        let fscrypt_dir_ino =
722            resolve_inode_path(&f2fs, "/fscrypt").await.expect("resolve encrypted dir");
723        let entries = f2fs.readdir(fscrypt_dir_ino).await.expect("readdir");
724        println!("entries {entries:?}");
725
726        for entry in entries {
727            assert!(expected.remove(entry.filename.as_str()), "unexpected entry {entry:?}");
728        }
729        assert!(expected.is_empty());
730
731        resolve_inode_path(&f2fs, &format!("/fscrypt/{str_a}"))
732            .await
733            .expect("resolve encrypted dir");
734        let enc_symlink_ino =
735            resolve_inode_path(&f2fs, &format!("/fscrypt/{str_a}/{str_b}/{str_symlink}"))
736                .await
737                .expect("resolve encrypted symlink");
738        let symlink_inode =
739            Inode::try_load(&f2fs, enc_symlink_ino).await.expect("load symlink inode");
740        assert_eq!(
741            &*f2fs.read_symlink(&symlink_inode).expect("read_symlink"),
742            bytes_symlink_content
743        );
744
745        // ...now try with the key
746        f2fs.add_key(&[0u8; 64]);
747        resolve_inode_path(&f2fs, "/fscrypt/a/b/regular").await.expect("resolve fscrypt regular");
748        let inlined_ino = resolve_inode_path(&f2fs, "/fscrypt/a/b/inlined")
749            .await
750            .expect("resolve fscrypt inlined");
751        let short_file = Inode::try_load(&f2fs, inlined_ino).await.expect("load symlink inode");
752        assert!(
753            !short_file.header.inline_flags.contains(inode::InlineFlags::Data),
754            "encrypted files shouldn't be inlined"
755        );
756        let short_data =
757            f2fs.read_data(&short_file, 0).await.expect("read_data").expect("non-empty page");
758        assert_eq!(&short_data[..short_file.header.size as usize], b"test45678abcdef_12345678");
759
760        let symlink_ino = resolve_inode_path(&f2fs, "/fscrypt/a/b/symlink")
761            .await
762            .expect("resolve fscrypt symlink");
763        assert_eq!(symlink_ino, enc_symlink_ino);
764
765        let symlink_inode = Inode::try_load(&f2fs, symlink_ino).await.expect("load symlink inode");
766        let symlink = f2fs.read_symlink(&symlink_inode).expect("read_symlink");
767        assert_eq!(*symlink, *b"inlined");
768    }
769
770    #[fuchsia::test]
771    async fn test_summary_block_addr() {
772        let device = open_test_image("/pkg/testdata/f2fs.img.zst");
773        let mut f2fs = F2fsReader::open_device(Arc::new(device)).await.expect("open ok");
774
775        // Case 1: No Orphan Flag
776        f2fs.checkpoint.header.ckpt_flags = 0; // Clear all
777        f2fs.checkpoint.header.cp_pack_start_sum = 100;
778        let base = f2fs.checkpoint_start_addr();
779        assert_eq!(f2fs.summary_block_addr(), base + 100);
780
781        // Case 2: With Orphan Flag
782        f2fs.checkpoint.header.ckpt_flags = CP_ORPHAN_PRESENT_FLAG;
783        assert_eq!(f2fs.summary_block_addr(), base + 100 + 1);
784
785        // Case 3: Compact Summary + Orphan (Real crash case)
786        f2fs.checkpoint.header.ckpt_flags = CP_ORPHAN_PRESENT_FLAG | CKPT_FLAG_COMPACT_SUMMARY;
787        assert_eq!(f2fs.summary_block_addr(), base + 100 + 1);
788    }
789}