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