1use std::collections::HashMap;
8use std::fmt;
9use std::fs::{File, OpenOptions};
10use std::io::Result;
11use std::os::unix::io::AsRawFd;
12use std::path::Path;
13use std::sync::{Arc, RwLock};
14
15use fuse_backend_rs::file_buf::FileVolatileSlice;
16use nix::sys::uio;
17use nydus_api::LocalDiskConfig;
18use nydus_utils::metrics::BackendMetrics;
19
20use crate::backend::{BackendError, BackendResult, BlobBackend, BlobReader};
21use crate::utils::{readv, MemSliceCursor};
22
23type LocalDiskResult<T> = std::result::Result<T, LocalDiskError>;
24
25#[derive(Debug)]
27pub enum LocalDiskError {
28 BlobFile(String),
29 ReadBlob(String),
30}
31
32impl fmt::Display for LocalDiskError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 LocalDiskError::BlobFile(s) => write!(f, "{}", s),
36 LocalDiskError::ReadBlob(s) => write!(f, "{}", s),
37 }
38 }
39}
40
41impl From<LocalDiskError> for BackendError {
42 fn from(error: LocalDiskError) -> Self {
43 BackendError::LocalDisk(error)
44 }
45}
46
47#[derive(Debug)]
48struct LocalDiskBlob {
49 device_file: File,
51 blob_offset: u64,
53 blob_length: u64,
55 blob_id: String,
57 metrics: Arc<BackendMetrics>,
59}
60
61impl BlobReader for LocalDiskBlob {
62 fn blob_size(&self) -> BackendResult<u64> {
63 Ok(self.blob_length)
64 }
65
66 fn try_read(&self, buf: &mut [u8], offset: u64) -> BackendResult<usize> {
67 let msg = format!(
68 "localdisk: invalid offset 0x{:x}, base 0x{:x}, length 0x{:x}",
69 offset, self.blob_offset, self.blob_length
70 );
71 if offset >= self.blob_length {
72 return Ok(0);
73 }
74 let actual_offset = self
75 .blob_offset
76 .checked_add(offset)
77 .ok_or(LocalDiskError::ReadBlob(msg))?;
78 let len = std::cmp::min(self.blob_length - offset, buf.len() as u64) as usize;
79
80 uio::pread(
81 self.device_file.as_raw_fd(),
82 &mut buf[..len],
83 actual_offset as i64,
84 )
85 .map_err(|e| {
86 let msg = format!(
87 "localdisk: failed to read data from blob {}, {}",
88 self.blob_id, e
89 );
90 LocalDiskError::ReadBlob(msg).into()
91 })
92 }
93
94 fn readv(
95 &self,
96 bufs: &[FileVolatileSlice],
97 offset: u64,
98 max_size: usize,
99 ) -> BackendResult<usize> {
100 let msg = format!(
101 "localdisk: invalid offset 0x{:x}, base 0x{:x}, length 0x{:x}",
102 offset, self.blob_offset, self.blob_length
103 );
104 if offset >= self.blob_length {
105 return Ok(0);
106 }
107 let actual_offset = self
108 .blob_offset
109 .checked_add(offset)
110 .ok_or(LocalDiskError::ReadBlob(msg.clone()))?;
111
112 let mut c = MemSliceCursor::new(bufs);
113 let mut iovec = c.consume(max_size);
114 let mut len = 0;
115 for buf in bufs {
116 len += buf.len();
117 }
118
119 if offset.checked_add(len as u64).is_none() || offset + len as u64 > self.blob_length {
121 return Err(LocalDiskError::ReadBlob(msg).into());
122 }
123
124 readv(self.device_file.as_raw_fd(), &mut iovec, actual_offset).map_err(|e| {
125 let msg = format!(
126 "localdisk: failed to read data from blob {}, {}",
127 self.blob_id, e
128 );
129 LocalDiskError::ReadBlob(msg).into()
130 })
131 }
132
133 fn metrics(&self) -> &BackendMetrics {
134 &self.metrics
135 }
136}
137
138pub struct LocalDisk {
140 device_file: File,
142 device_path: String,
144 device_capacity: u64,
146 is_gpt_mode: bool,
148 metrics: Arc<BackendMetrics>,
150 entries: RwLock<HashMap<String, Arc<LocalDiskBlob>>>,
152}
153
154impl LocalDisk {
155 pub fn new(config: &LocalDiskConfig, id: Option<&str>) -> Result<LocalDisk> {
156 let id = id.ok_or_else(|| einval!("localdisk: argument `id` is empty"))?;
157 let path = &config.device_path;
158 let path_buf = Path::new(path).to_path_buf().canonicalize().map_err(|e| {
159 einval!(format!(
160 "localdisk: invalid disk device path {}, {}",
161 path, e
162 ))
163 })?;
164 let device_file = OpenOptions::new().read(true).open(path_buf).map_err(|e| {
165 einval!(format!(
166 "localdisk: can not open disk device at {}, {}",
167 path, e
168 ))
169 })?;
170 let md = device_file.metadata().map_err(|e| {
171 eio!(format!(
172 "localdisk: can not get file meta data about disk device {}, {}",
173 path, e
174 ))
175 })?;
176 let mut local_disk = LocalDisk {
177 device_file,
178 device_path: path.clone(),
179 device_capacity: md.len(),
180 is_gpt_mode: false,
181 metrics: BackendMetrics::new(id, "localdisk"),
182 entries: RwLock::new(HashMap::new()),
183 };
184
185 if !config.disable_gpt {
186 local_disk.scan_blobs_by_gpt()?;
187 }
188
189 Ok(local_disk)
190 }
191
192 pub fn add_blob(&self, blob_id: &str, offset: u64, length: u64) -> LocalDiskResult<()> {
193 if self.is_gpt_mode {
194 let msg = format!(
195 "localdisk: device {} is in legacy gpt mode",
196 self.device_path
197 );
198 return Err(LocalDiskError::BlobFile(msg));
199 }
200 if offset.checked_add(length).is_none() || offset + length > self.device_capacity {
201 let msg = format!(
202 "localdisk: add blob {} with invalid offset 0x{:x} and length 0x{:x}, device size 0x{:x}",
203 blob_id, offset, length, self.device_capacity
204 );
205 return Err(LocalDiskError::BlobFile(msg));
206 };
207
208 let device_file = self.device_file.try_clone().map_err(|e| {
209 LocalDiskError::BlobFile(format!("localdisk: can not duplicate file, {}", e))
210 })?;
211 let blob = Arc::new(LocalDiskBlob {
212 blob_id: blob_id.to_string(),
213 device_file,
214 blob_offset: offset,
215 blob_length: length,
216 metrics: self.metrics.clone(),
217 });
218
219 let mut table_guard = self.entries.write().unwrap();
220 if table_guard.contains_key(blob_id) {
221 let msg = format!("localdisk: blob {} already exists", blob_id);
222 return Err(LocalDiskError::BlobFile(msg));
223 }
224 table_guard.insert(blob_id.to_string(), blob);
225
226 Ok(())
227 }
228
229 fn get_blob(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
230 if let Some(entry) = self.entries.read().unwrap().get(blob_id) {
232 Ok(entry.clone())
233 } else {
234 self.get_blob_from_gpt(blob_id)
235 }
236 }
237}
238
239#[cfg(feature = "backend-localdisk-gpt")]
240impl LocalDisk {
241 fn truncate_blob_id(blob_id: &str) -> Option<&str> {
243 const LOCALDISK_BLOB_ID_LEN: usize = 32;
244 if blob_id.len() >= LOCALDISK_BLOB_ID_LEN {
245 let new_blob_id = &blob_id[0..LOCALDISK_BLOB_ID_LEN];
246 Some(new_blob_id)
247 } else {
248 None
249 }
250 }
251
252 fn get_blob_from_gpt(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
253 if self.is_gpt_mode {
254 if let Some(localdisk_blob_id) = LocalDisk::truncate_blob_id(blob_id) {
255 if let Some(entry) = self.entries.read().unwrap().get(localdisk_blob_id) {
257 return Ok(entry.clone());
258 }
259 }
260 }
261
262 let msg = format!("localdisk: can not find such blob: {}", blob_id);
263 Err(LocalDiskError::ReadBlob(msg))
264 }
265
266 fn scan_blobs_by_gpt(&mut self) -> Result<()> {
267 let cfg = gpt::GptConfig::new().writable(false);
269 let disk = cfg.open(&self.device_path)?;
270 let partitions = disk.partitions();
271 let sector_size = gpt::disk::DEFAULT_SECTOR_SIZE;
272 info!(
273 "Localdisk initializing storage backend for device {} with {} partitions, GUID: {}",
274 self.device_path,
275 partitions.len(),
276 disk.guid()
277 );
278
279 let mut table_guard = self.entries.write().unwrap();
280 for (k, v) in partitions {
281 let length = v.bytes_len(sector_size)?;
282 let base_offset = v.bytes_start(sector_size)?;
283 if base_offset.checked_add(length).is_none()
284 || base_offset + length > self.device_capacity
285 {
286 let msg = format!(
287 "localdisk: partition {} with invalid offset and length",
288 v.part_guid
289 );
290 return Err(einval!(msg));
291 };
292 let guid = v.part_guid;
293 let mut is_gpt_mode = false;
294 let name = if v.part_type_guid == gpt::partition_types::BASIC {
295 is_gpt_mode = true;
296 v.name.clone()
298 } else {
299 v.name.clone() + guid.simple().to_string().as_str()
301 };
302
303 if name.is_empty() {
304 let msg = format!("localdisk: partition {} has empty blob id", v.part_guid);
305 return Err(einval!(msg));
306 }
307 if table_guard.contains_key(&name) {
308 let msg = format!("localdisk: blob {} already exists", name);
309 return Err(einval!(msg));
310 }
311
312 let device_file = self.device_file.try_clone()?;
313 let partition = Arc::new(LocalDiskBlob {
314 blob_id: name.clone(),
315 device_file,
316 blob_offset: base_offset,
317 blob_length: length,
318 metrics: self.metrics.clone(),
319 });
320
321 debug!(
322 "Localdisk partition {} initialized, blob id: {}, offset {}, length {}",
323 k, partition.blob_id, partition.blob_offset, partition.blob_length
324 );
325 table_guard.insert(name, partition);
326 if is_gpt_mode {
327 self.is_gpt_mode = true;
328 }
329 }
330
331 Ok(())
332 }
333}
334
335#[cfg(not(feature = "backend-localdisk-gpt"))]
336impl LocalDisk {
337 fn get_blob_from_gpt(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
338 Err(LocalDiskError::ReadBlob(format!(
339 "can not find such blob: {}, this image might be corrupted",
340 blob_id
341 )))
342 }
343
344 fn scan_blobs_by_gpt(&mut self) -> Result<()> {
345 Ok(())
346 }
347}
348
349impl BlobBackend for LocalDisk {
350 fn shutdown(&self) {}
351
352 fn metrics(&self) -> &BackendMetrics {
353 &self.metrics
354 }
355
356 fn get_reader(&self, blob_id: &str) -> BackendResult<Arc<dyn BlobReader>> {
357 self.get_blob(blob_id).map_err(|e| e.into())
358 }
359}
360
361impl Drop for LocalDisk {
362 fn drop(&mut self) {
363 self.metrics.release().unwrap_or_else(|e| error!("{:?}", e));
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_invalid_localdisk_new() {
373 let config = LocalDiskConfig {
374 device_path: "".to_string(),
375 disable_gpt: true,
376 };
377 assert!(LocalDisk::new(&config, Some("test")).is_err());
378
379 let config = LocalDiskConfig {
380 device_path: "/a/b/c".to_string(),
381 disable_gpt: true,
382 };
383 assert!(LocalDisk::new(&config, None).is_err());
384 }
385
386 #[test]
387 fn test_add_disk_blob() {
388 let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
389 let root_dir = Path::new(root_dir).join("../tests/texture/blobs/");
390
391 let config = LocalDiskConfig {
392 device_path: root_dir.join("nonexist_blob_file").display().to_string(),
393 disable_gpt: true,
394 };
395 assert!(LocalDisk::new(&config, Some("test")).is_err());
396
397 let blob_id = "be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef";
398 let config = LocalDiskConfig {
399 device_path: root_dir.join(blob_id).display().to_string(),
400 disable_gpt: true,
401 };
402 let disk = LocalDisk::new(&config, Some("test")).unwrap();
403
404 assert!(disk.add_blob(blob_id, u64::MAX, 1).is_err());
405 assert!(disk.add_blob(blob_id, 14553, 2).is_err());
406 assert!(disk.add_blob(blob_id, 14554, 1).is_err());
407 assert!(disk.add_blob(blob_id, 0, 4096).is_ok());
408 assert!(disk.add_blob(blob_id, 0, 4096).is_err());
409 let blob = disk.get_blob(blob_id).unwrap();
410 assert_eq!(blob.blob_size().unwrap(), 4096);
411
412 let mut buf = vec![0u8; 4096];
413 let sz = blob.read(&mut buf, 0).unwrap();
414 assert_eq!(sz, 4096);
415 let sz = blob.read(&mut buf, 4095).unwrap();
416 assert_eq!(sz, 1);
417 let sz = blob.read(&mut buf, 4096).unwrap();
418 assert_eq!(sz, 0);
419 let sz = blob.read(&mut buf, 4097).unwrap();
420 assert_eq!(sz, 0);
421 }
422
423 #[cfg(feature = "backend-localdisk-gpt")]
424 #[test]
425 fn test_truncate_blob_id() {
426 let guid = "50ad3c8243e0a08ecdebde0ef8afcc6f2abca44498ad15491acbe58c83acb66f";
427 let guid_truncated = "50ad3c8243e0a08ecdebde0ef8afcc6f";
428
429 let result = LocalDisk::truncate_blob_id(guid).unwrap();
430 assert_eq!(result, guid_truncated)
431 }
432}