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}