Skip to main content

gobblytes_erofs/
lib.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result, anyhow, bail};
4use async_trait::async_trait;
5use gibblox_core::{
6    AlignedByteReader, BlockReader, GibbloxErrorKind, LruBlockReader, PagedBlockReader, ReadContext,
7};
8use gobblytes_core::{Filesystem, FilesystemEntryType};
9
10const DIRENT_SIZE: usize = 12;
11pub const DEFAULT_IMAGE_BLOCK_SIZE: u32 = 512;
12
13#[derive(Clone)]
14pub struct ErofsRootfs {
15    fs: erofs_rs::EroFS<GibbloxReadAtAdapter>,
16}
17
18impl ErofsRootfs {
19    pub async fn new(reader: Arc<dyn BlockReader>, image_size_bytes: u64) -> Result<Self> {
20        let lru = LruBlockReader::new(reader, Default::default())
21            .await
22            .map_err(|err| anyhow!("initialize LRU for rootfs reader: {err}"))?;
23        let paged = PagedBlockReader::new(lru, Default::default())
24            .await
25            .map_err(|err| anyhow!("initialize paged reader for rootfs reader: {err}"))?;
26
27        let source_block_size = paged.block_size();
28        if source_block_size == 0 || !source_block_size.is_power_of_two() {
29            bail!("source block size must be non-zero power of two");
30        }
31        let adapter = GibbloxReadAtAdapter {
32            byte_reader: AlignedByteReader::new(Arc::new(paged)).await?,
33        };
34        let fs = erofs_rs::EroFS::from_image(adapter, image_size_bytes)
35            .await
36            .map_err(|err| anyhow!("open erofs image: {err}"))?;
37        Ok(Self { fs })
38    }
39
40    fn normalize(path: &str) -> String {
41        let trimmed = path.trim();
42        let inner = trimmed.trim_start_matches('/');
43        if inner.is_empty() {
44            "/".to_string()
45        } else {
46            format!("/{inner}")
47        }
48    }
49
50    async fn resolve_inode(&self, path: &str) -> Result<Option<erofs_rs::types::Inode>> {
51        self.fs
52            .get_path_inode_str(path)
53            .await
54            .map_err(|err| anyhow!("resolve EROFS path {path}: {err}"))
55    }
56
57    async fn read_symlink_target(
58        &self,
59        inode: &erofs_rs::types::Inode,
60        path: &str,
61    ) -> Result<String> {
62        let mut out = vec![0u8; inode.data_size()];
63        let read = self
64            .fs
65            .read_inode_range(inode, 0, &mut out)
66            .await
67            .map_err(|err| anyhow!("read EROFS symlink {path}: {err}"))?;
68        out.truncate(read);
69
70        let target = core::str::from_utf8(&out)
71            .map_err(|_| anyhow!("symlink target is not UTF-8 for {path}"))?
72            .trim_end_matches('\0')
73            .trim();
74        if target.is_empty() {
75            bail!("symlink target is empty for {path}");
76        }
77        Ok(target.to_string())
78    }
79}
80
81impl Filesystem for ErofsRootfs {
82    type Error = anyhow::Error;
83
84    async fn read_all(&self, path: &str) -> Result<Vec<u8>> {
85        let normalized = Self::normalize(path);
86        let inode = self
87            .resolve_inode(&normalized)
88            .await?
89            .ok_or_else(|| anyhow!("missing path {normalized}"))?;
90        if !inode.is_file() {
91            bail!("path is not a regular file: {normalized}");
92        }
93
94        let mut out = vec![0u8; inode.data_size()];
95        let mut offset = 0usize;
96        while offset < out.len() {
97            let read = self
98                .fs
99                .read_inode_range(&inode, offset, &mut out[offset..])
100                .await
101                .map_err(|err| anyhow!("read EROFS file {normalized}: {err}"))?;
102            if read == 0 {
103                break;
104            }
105            offset += read;
106        }
107        out.truncate(offset);
108        Ok(out)
109    }
110
111    async fn read_range(&self, path: &str, offset: u64, len: usize) -> Result<Vec<u8>> {
112        if len == 0 {
113            return Ok(Vec::new());
114        }
115        let normalized = Self::normalize(path);
116        let inode = self
117            .resolve_inode(&normalized)
118            .await?
119            .ok_or_else(|| anyhow!("missing path {normalized}"))?;
120        if !inode.is_file() {
121            bail!("path is not a regular file: {normalized}");
122        }
123        let file_size = inode.data_size();
124        let offset = usize::try_from(offset).context("range offset exceeds usize")?;
125        if offset >= file_size {
126            return Ok(Vec::new());
127        }
128        let read_len = len.min(file_size - offset);
129        let mut out = vec![0u8; read_len];
130        let read = self
131            .fs
132            .read_inode_range(&inode, offset, &mut out)
133            .await
134            .map_err(|err| anyhow!("read range in EROFS file {normalized}: {err}"))?;
135        out.truncate(read);
136        Ok(out)
137    }
138
139    async fn read_dir(&self, path: &str) -> Result<Vec<String>> {
140        let normalized = Self::normalize(path);
141        let inode = self
142            .resolve_inode(&normalized)
143            .await?
144            .ok_or_else(|| anyhow!("missing path {normalized}"))?;
145        if !inode.is_dir() {
146            bail!("path is not a directory: {normalized}");
147        }
148
149        let block_size = self.fs.block_size();
150        let data_size = inode.data_size();
151        let mut names = Vec::new();
152        let mut offset = 0usize;
153        while offset < data_size {
154            let mut block = vec![0u8; (data_size - offset).min(block_size)];
155            let read = self
156                .fs
157                .read_inode_range(&inode, offset, &mut block)
158                .await
159                .map_err(|err| anyhow!("read EROFS directory {normalized}: {err}"))?;
160            block.truncate(read);
161            parse_dir_block(&block, &mut names)
162                .with_context(|| format!("parse EROFS directory block for {normalized}"))?;
163            offset = offset.saturating_add(block_size);
164        }
165
166        Ok(names)
167    }
168
169    async fn entry_type(&self, path: &str) -> Result<Option<FilesystemEntryType>> {
170        let normalized = Self::normalize(path);
171        let inode = match self.resolve_inode(&normalized).await? {
172            Some(inode) => inode,
173            None => return Ok(None),
174        };
175        let ty = if inode.is_file() {
176            FilesystemEntryType::File
177        } else if inode.is_dir() {
178            FilesystemEntryType::Directory
179        } else if inode.is_symlink() {
180            FilesystemEntryType::Symlink
181        } else {
182            FilesystemEntryType::Other
183        };
184        Ok(Some(ty))
185    }
186
187    async fn read_link(&self, path: &str) -> Result<String> {
188        let normalized = Self::normalize(path);
189        let inode = self
190            .resolve_inode(&normalized)
191            .await?
192            .ok_or_else(|| anyhow!("missing path {normalized}"))?;
193        if !inode.is_symlink() {
194            bail!("path is not a symlink: {normalized}");
195        }
196        self.read_symlink_target(&inode, &normalized).await
197    }
198
199    async fn exists(&self, path: &str) -> Result<bool> {
200        let normalized = Self::normalize(path);
201        Ok(self.resolve_inode(&normalized).await?.is_some())
202    }
203}
204
205#[derive(Clone)]
206struct GibbloxReadAtAdapter {
207    byte_reader: AlignedByteReader,
208}
209
210#[async_trait]
211impl erofs_rs::ReadAt for GibbloxReadAtAdapter {
212    async fn read_at(&self, offset: u64, buf: &mut [u8]) -> erofs_rs::Result<usize> {
213        if buf.is_empty() {
214            return Ok(0);
215        }
216
217        self.byte_reader
218            .read_exact_at(offset, buf, ReadContext::FOREGROUND)
219            .await
220            .map_err(map_gibblox_err)?;
221        Ok(buf.len())
222    }
223}
224
225fn parse_dir_block(block: &[u8], out: &mut Vec<String>) -> Result<()> {
226    if block.len() < DIRENT_SIZE {
227        return Ok(());
228    }
229
230    let entry_count = u16::from_le_bytes([block[8], block[9]]) as usize / DIRENT_SIZE;
231    if entry_count == 0 {
232        return Ok(());
233    }
234
235    for index in 0..entry_count {
236        let entry_start = index * DIRENT_SIZE;
237        let name_off_start = entry_start + 8;
238        if name_off_start + 2 > block.len() {
239            break;
240        }
241        let name_start =
242            u16::from_le_bytes([block[name_off_start], block[name_off_start + 1]]) as usize;
243        let name_end = if index + 1 < entry_count {
244            let next_start = (index + 1) * DIRENT_SIZE + 8;
245            if next_start + 2 > block.len() {
246                block.len()
247            } else {
248                u16::from_le_bytes([block[next_start], block[next_start + 1]]) as usize
249            }
250        } else {
251            block.len()
252        };
253        if name_end < name_start || name_end > block.len() {
254            bail!("invalid directory name range");
255        }
256        let name = String::from_utf8_lossy(&block[name_start..name_end])
257            .trim_end_matches('\0')
258            .to_string();
259        if name.is_empty() || name == "." || name == ".." {
260            continue;
261        }
262        out.push(name);
263    }
264
265    Ok(())
266}
267
268fn map_gibblox_err(err: gibblox_core::GibbloxError) -> erofs_rs::Error {
269    match err.kind() {
270        GibbloxErrorKind::InvalidInput => {
271            erofs_rs::Error::CorruptedData(format!("invalid input: {err}"))
272        }
273        GibbloxErrorKind::OutOfRange => erofs_rs::Error::OutOfBounds(err.to_string()),
274        GibbloxErrorKind::Io => erofs_rs::Error::OutOfBounds(err.to_string()),
275        GibbloxErrorKind::Unsupported => erofs_rs::Error::NotSupported(err.to_string()),
276        GibbloxErrorKind::Other => erofs_rs::Error::OutOfBounds(err.to_string()),
277    }
278}