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