|
| 1 | +// Copyright 2026 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +use std::fs::File; |
| 16 | +use std::io::IoSliceMut; |
| 17 | + |
| 18 | +use alioth::fuse::Fuse; |
| 19 | +use alioth::fuse::bindings::{ |
| 20 | + FUSE_ROOT_ID, FuseDirent, FuseDirentType, FuseInHeader, FuseOpcode, FuseOpenIn, FuseReadIn, |
| 21 | +}; |
| 22 | +use alioth::fuse::passthrough::Passthrough; |
| 23 | +use tempfile::TempDir; |
| 24 | +use zerocopy::FromBytes; |
| 25 | + |
| 26 | +const NUM_FILES: usize = 6; |
| 27 | +// 24-byte FuseDirent + "file<N>" (5 bytes) padded to 8 |
| 28 | +const DIRENT_SIZE: usize = 32; |
| 29 | + |
| 30 | +fn hdr(opcode: FuseOpcode) -> FuseInHeader { |
| 31 | + FuseInHeader { |
| 32 | + len: 0, |
| 33 | + opcode, |
| 34 | + unique: 0, |
| 35 | + nodeid: FUSE_ROOT_ID, |
| 36 | + uid: 0, |
| 37 | + gid: 0, |
| 38 | + pid: 0, |
| 39 | + total_extlen: 0, |
| 40 | + padding: 0, |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +fn create_files(dir: &TempDir) -> Vec<String> { |
| 45 | + (0..NUM_FILES) |
| 46 | + .map(|i| { |
| 47 | + let name = format!("file{i}"); |
| 48 | + File::create(dir.path().join(&name)).unwrap(); |
| 49 | + name |
| 50 | + }) |
| 51 | + .collect() |
| 52 | +} |
| 53 | + |
| 54 | +/// Parses dirents from a contiguous reply buffer of length `buf.len()`. |
| 55 | +fn parse_dirents(mut buf: &[u8]) -> (Vec<String>, Vec<u64>) { |
| 56 | + let mut names = vec![]; |
| 57 | + let mut offs = vec![]; |
| 58 | + while !buf.is_empty() { |
| 59 | + let (dirent, rest) = FuseDirent::ref_from_prefix(buf).unwrap(); |
| 60 | + let namelen = dirent.namelen as usize; |
| 61 | + assert!(namelen > 0); |
| 62 | + assert_eq!(dirent.type_, FuseDirentType::REG); |
| 63 | + let aligned_namelen = (namelen + 7) & !7; |
| 64 | + assert_eq!( |
| 65 | + &rest[namelen..aligned_namelen], |
| 66 | + &[0u8; 8][..aligned_namelen - namelen] |
| 67 | + ); |
| 68 | + names.push(String::from_utf8(rest[..namelen].to_vec()).unwrap()); |
| 69 | + offs.push(dirent.off); |
| 70 | + buf = &rest[aligned_namelen..]; |
| 71 | + } |
| 72 | + (names, offs) |
| 73 | +} |
| 74 | + |
| 75 | +fn read_dir(dir: &TempDir, slice_lens: &[usize]) -> (usize, Vec<String>) { |
| 76 | + let mut fs = Passthrough::new(dir.path().into()).unwrap(); |
| 77 | + let open = fs |
| 78 | + .open_dir(&hdr(FuseOpcode::OPENDIR), &FuseOpenIn::default()) |
| 79 | + .unwrap(); |
| 80 | + |
| 81 | + let mut storage: Vec<Vec<u8>> = slice_lens.iter().map(|&len| vec![0u8; len]).collect(); |
| 82 | + let mut slices: Vec<IoSliceMut> = storage.iter_mut().map(|b| IoSliceMut::new(b)).collect(); |
| 83 | + let total = fs |
| 84 | + .read_dir( |
| 85 | + &hdr(FuseOpcode::READDIR), |
| 86 | + &FuseReadIn { |
| 87 | + fh: open.fh, |
| 88 | + ..Default::default() |
| 89 | + }, |
| 90 | + &mut slices, |
| 91 | + ) |
| 92 | + .unwrap(); |
| 93 | + |
| 94 | + let flat: Vec<u8> = storage.iter().flatten().copied().collect(); |
| 95 | + assert!(flat[total..].iter().all(|&b| b == 0)); |
| 96 | + let (mut names, offs) = parse_dirents(&flat[..total]); |
| 97 | + assert_eq!(offs, (1..=offs.len() as u64).collect::<Vec<_>>()); |
| 98 | + names.sort(); |
| 99 | + (total, names) |
| 100 | +} |
| 101 | + |
| 102 | +#[test] |
| 103 | +fn read_dir_single_buffer_test() { |
| 104 | + let dir = TempDir::new().unwrap(); |
| 105 | + let expected = create_files(&dir); |
| 106 | + let (total, names) = read_dir(&dir, &[4096]); |
| 107 | + assert_eq!(total, NUM_FILES * DIRENT_SIZE); |
| 108 | + assert_eq!(names, expected); |
| 109 | +} |
| 110 | + |
| 111 | +#[test] |
| 112 | +fn read_dir_multiple_buffers_test() { |
| 113 | + let dir = TempDir::new().unwrap(); |
| 114 | + let expected = create_files(&dir); |
| 115 | + let (total, names) = read_dir(&dir, &[64, 64, 64]); |
| 116 | + assert_eq!(total, NUM_FILES * DIRENT_SIZE); |
| 117 | + assert_eq!(names, expected); |
| 118 | +} |
| 119 | + |
| 120 | +#[test] |
| 121 | +fn read_dir_straddles_buffer_boundary_test() { |
| 122 | + // A 40-byte buffer fits one 32-byte dirent and the first 8 bytes of the |
| 123 | + // next dirent, whose remaining 24 bytes spill into the following buffer. |
| 124 | + let dir = TempDir::new().unwrap(); |
| 125 | + let expected = create_files(&dir); |
| 126 | + let (total, names) = read_dir(&dir, &[40; NUM_FILES]); |
| 127 | + assert_eq!(total, NUM_FILES * DIRENT_SIZE); |
| 128 | + assert_eq!(names, expected); |
| 129 | +} |
| 130 | + |
| 131 | +#[test] |
| 132 | +fn read_dir_stops_when_buffers_full_test() { |
| 133 | + let dir = TempDir::new().unwrap(); |
| 134 | + let expected = create_files(&dir); |
| 135 | + let (total, names) = read_dir(&dir, &[50, 50, 40]); |
| 136 | + assert_eq!(total, 4 * DIRENT_SIZE); |
| 137 | + assert_eq!(names.len(), 4); |
| 138 | + assert!(names.iter().all(|n| expected.contains(n))); |
| 139 | +} |
0 commit comments