nydus_builder/core/
node.rs

1// Copyright 2020 Ant Group. All rights reserved.
2// Copyright (C) 2021-2023 Alibaba Cloud. All rights reserved.
3//
4// SPDX-License-Identifier: Apache-2.0
5
6use std::ffi::{OsStr, OsString};
7use std::fmt::{self, Display, Formatter, Result as FmtResult};
8use std::fs::{self, File};
9use std::io::Read;
10use std::ops::Deref;
11#[cfg(target_os = "linux")]
12use std::os::linux::fs::MetadataExt;
13#[cfg(target_os = "macos")]
14use std::os::macos::fs::MetadataExt;
15use std::os::unix::ffi::OsStrExt;
16use std::path::{Component, Path, PathBuf};
17use std::sync::Arc;
18
19use anyhow::{anyhow, bail, Context, Error, Result};
20use nydus_rafs::metadata::chunk::ChunkWrapper;
21use nydus_rafs::metadata::inode::InodeWrapper;
22use nydus_rafs::metadata::layout::v6::EROFS_INODE_FLAT_PLAIN;
23use nydus_rafs::metadata::layout::RafsXAttrs;
24use nydus_rafs::metadata::{Inode, RafsVersion};
25use nydus_storage::device::BlobFeatures;
26use nydus_storage::meta::{BlobChunkInfoV2Ondisk, BlobMetaChunkInfo};
27use nydus_utils::digest::{DigestHasher, RafsDigest};
28use nydus_utils::{compress, crc32, crypt};
29use nydus_utils::{div_round_up, event_tracer, root_tracer, try_round_up_4k, ByteSize};
30use parse_size::parse_size;
31use sha2::digest::Digest;
32
33use crate::{BlobContext, BlobManager, BuildContext, ChunkDict, ConversionType, Overlay};
34
35use super::context::Artifact;
36
37/// Filesystem root path for Unix OSs.
38const ROOT_PATH_NAME: &[u8] = b"/";
39
40/// Source of chunk data: chunk dictionary, parent filesystem or builder.
41#[derive(Clone, Hash, PartialEq, Eq)]
42pub enum ChunkSource {
43    /// Chunk is stored in data blob owned by current image.
44    Build,
45    /// A reference to a chunk in chunk dictionary.
46    Dict,
47    /// A reference to a chunk in parent image.
48    Parent,
49}
50
51impl Display for ChunkSource {
52    fn fmt(&self, f: &mut Formatter) -> FmtResult {
53        match self {
54            Self::Build => write!(f, "build"),
55            Self::Dict => write!(f, "dict"),
56            Self::Parent => write!(f, "parent"),
57        }
58    }
59}
60
61/// Chunk information for RAFS filesystem builder.
62#[derive(Clone)]
63pub struct NodeChunk {
64    pub source: ChunkSource,
65    pub inner: Arc<ChunkWrapper>,
66}
67
68impl Display for NodeChunk {
69    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
70        write!(f, "{}", self.inner,)
71    }
72}
73
74impl NodeChunk {
75    /// Copy all chunk information from another `ChunkWrapper` object.
76    pub fn copy_from(&mut self, other: &ChunkWrapper) {
77        let mut chunk = self.inner.deref().clone();
78        chunk.copy_from(other);
79        self.inner = Arc::new(chunk);
80    }
81
82    /// Set chunk index.
83    pub fn set_index(&mut self, index: u32) {
84        let mut chunk = self.inner.deref().clone();
85        chunk.set_index(index);
86        self.inner = Arc::new(chunk);
87    }
88
89    /// Set blob index.
90    pub fn set_blob_index(&mut self, index: u32) {
91        let mut chunk = self.inner.deref().clone();
92        chunk.set_blob_index(index);
93        self.inner = Arc::new(chunk);
94    }
95
96    /// Set chunk compressed size.
97    pub fn set_compressed_size(&mut self, size: u32) {
98        let mut chunk = self.inner.deref().clone();
99        chunk.set_compressed_size(size);
100        self.inner = Arc::new(chunk);
101    }
102
103    /// Set file offset of chunk.
104    pub fn set_file_offset(&mut self, offset: u64) {
105        let mut chunk = self.inner.deref().clone();
106        chunk.set_file_offset(offset);
107        self.inner = Arc::new(chunk);
108    }
109}
110
111/// Struct to host sharable fields of [Node].
112#[derive(Clone, Default, Debug)]
113pub struct NodeInfo {
114    /// Whether the explicit UID/GID feature is enabled or not.
115    pub explicit_uidgid: bool,
116
117    /// Device id associated with the source inode.
118    ///
119    /// A source directory may contain multiple partitions from different hard disk, so
120    /// a pair of (src_ino, src_dev) is needed to uniquely identify an inode from source directory.
121    pub src_dev: u64,
122    /// Inode number of the source inode, from fs stat().
123    pub src_ino: Inode,
124    /// Device ID for special files, describing the device that this inode represents.
125    pub rdev: u64,
126    /// Absolute path of the source root directory.
127    pub source: PathBuf,
128    /// Absolute path of the source file/directory.
129    pub path: PathBuf,
130    /// Absolute path within the target RAFS filesystem.
131    pub target: PathBuf,
132    /// Parsed version of `target`.
133    pub target_vec: Vec<OsString>,
134    /// Symlink info of symlink file
135    pub symlink: Option<OsString>,
136    /// Extended attributes.
137    pub xattrs: RafsXAttrs,
138
139    /// V6: whether it's forced to use an extended inode.
140    pub v6_force_extended_inode: bool,
141}
142
143/// An in-memory representation of RAFS inode for image building and inspection.
144#[derive(Clone)]
145pub struct Node {
146    /// Immutable fields of a Node object.
147    pub info: Arc<NodeInfo>,
148    /// Assigned RAFS inode number.
149    pub index: u64,
150    /// Define a disk inode structure to persist to disk.
151    pub inode: InodeWrapper,
152    /// Chunks info list of regular file
153    pub chunks: Vec<NodeChunk>,
154    /// Layer index where node is located.
155    pub layer_idx: u16,
156    /// Overlay type for layered build
157    pub overlay: Overlay,
158
159    /// V6: whether it's a compact inode or an extended inode.
160    pub v6_compact_inode: bool,
161    /// V6: inode data layout.
162    pub v6_datalayout: u16,
163    /// V6: offset to calculate nid.
164    pub v6_offset: u64,
165    /// V6: offset to build directory entries.
166    pub v6_dirents_offset: u64,
167    /// V6: information to build directory entries.
168    pub v6_dirents: Vec<(u64, OsString, u32)>,
169}
170
171impl Display for Node {
172    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
173        write!(
174            f,
175            "{} {:?}: index {} ino {} real_ino {} child_index {} child_count {} i_nlink {} i_size {} i_blocks {} i_name_size {} i_symlink_size {} has_xattr {} link {:?} i_mtime {} i_mtime_nsec {}",
176            self.file_type(),
177            self.target(),
178            self.index,
179            self.inode.ino(),
180            self.info.src_ino,
181            self.inode.child_index(),
182            self.inode.child_count(),
183            self.inode.nlink(),
184            self.inode.size(),
185            self.inode.blocks(),
186            self.inode.name_size(),
187            self.inode.symlink_size(),
188            self.inode.has_xattr(),
189            self.info.symlink,
190            self.inode.mtime(),
191            self.inode.mtime_nsec(),
192        )
193    }
194}
195
196impl Node {
197    /// Create a new instance of [Node].
198    pub fn new(inode: InodeWrapper, info: NodeInfo, layer_idx: u16) -> Self {
199        Node {
200            info: Arc::new(info),
201            index: 0,
202            overlay: Overlay::UpperAddition,
203            inode,
204            chunks: Vec::new(),
205            layer_idx,
206            v6_offset: 0,
207            v6_dirents: Vec::<(u64, OsString, u32)>::new(),
208            v6_datalayout: 0,
209            v6_compact_inode: false,
210            v6_dirents_offset: 0,
211        }
212    }
213
214    /// Dump node data into the data blob, and generate chunk information.
215    ///
216    /// # Arguments
217    /// - blob_writer: optional writer to write data into the data blob.
218    /// - data_buf: scratch buffer used to stored data read from the reader.
219    pub fn dump_node_data(
220        self: &mut Node,
221        ctx: &BuildContext,
222        blob_mgr: &mut BlobManager,
223        blob_writer: &mut dyn Artifact,
224        chunk_data_buf: &mut [u8],
225    ) -> Result<u64> {
226        let mut reader = if self.is_reg() {
227            let file = File::open(self.path())
228                .with_context(|| format!("failed to open node file {:?}", self.path()))?;
229            Some(file)
230        } else {
231            None
232        };
233
234        self.dump_node_data_with_reader(ctx, blob_mgr, blob_writer, reader.as_mut(), chunk_data_buf)
235    }
236
237    /// Dump data from a reader into the data blob, and generate chunk information.
238    ///
239    /// # Arguments
240    /// - blob_writer: optional writer to write data into the data blob.
241    /// - reader: reader to provide chunk data
242    /// - data_buf: scratch buffer used to stored data read from the reader.
243    pub fn dump_node_data_with_reader<R: Read>(
244        &mut self,
245        ctx: &BuildContext,
246        blob_mgr: &mut BlobManager,
247        blob_writer: &mut dyn Artifact,
248        reader: Option<&mut R>,
249        data_buf: &mut [u8],
250    ) -> Result<u64> {
251        if self.is_dir() {
252            return Ok(0);
253        } else if self.is_symlink() {
254            if let Some(symlink) = self.info.symlink.as_ref() {
255                if self.inode.is_v5() {
256                    self.inode
257                        .set_digest(RafsDigest::from_buf(symlink.as_bytes(), ctx.digester));
258                }
259                return Ok(0);
260            } else {
261                return Err(Error::msg("inode's symblink is invalid."));
262            }
263        } else if self.is_special() {
264            if self.inode.is_v5() {
265                self.inode
266                    .set_digest(RafsDigest::hasher(ctx.digester).digest_finalize());
267            }
268            return Ok(0);
269        }
270
271        let mut blob_size = 0u64;
272        let reader = reader.ok_or_else(|| anyhow!("missing reader to read file data"))?;
273        let mut inode_hasher = if self.inode.is_v5() {
274            Some(RafsDigest::hasher(ctx.digester))
275        } else {
276            None
277        };
278
279        if blob_mgr.external {
280            let external_values = ctx.attributes.get_values(self.target()).unwrap();
281            let external_blob_index = external_values
282                .get("blob_index")
283                .and_then(|v| v.parse::<u32>().ok())
284                .ok_or_else(|| anyhow!("failed to parse blob_index"))?;
285            let external_blob_id = external_values
286                .get("blob_id")
287                .ok_or_else(|| anyhow!("failed to parse blob_id"))?;
288            let external_chunk_size = external_values
289                .get("chunk_size")
290                .and_then(|v| parse_size(v).ok())
291                .ok_or_else(|| anyhow!("failed to parse chunk_size"))?;
292            let mut external_compressed_offset = external_values
293                .get("chunk_0_compressed_offset")
294                .and_then(|v| v.parse::<u64>().ok())
295                .ok_or_else(|| anyhow!("failed to parse chunk_0_compressed_offset"))?;
296            let external_compressed_size = external_values
297                .get("compressed_size")
298                .and_then(|v| v.parse::<u64>().ok())
299                .ok_or_else(|| anyhow!("failed to parse compressed_size"))?;
300            let (_, external_blob_ctx) =
301                blob_mgr.get_or_create_blob_by_idx(ctx, external_blob_index)?;
302            external_blob_ctx.blob_id = external_blob_id.to_string();
303            external_blob_ctx.compressed_blob_size = external_compressed_size;
304            external_blob_ctx.uncompressed_blob_size = external_compressed_size;
305            let chunk_count = self
306                .chunk_count(external_chunk_size as u64)
307                .with_context(|| {
308                    format!("failed to get chunk count for {}", self.path().display())
309                })?;
310            self.inode.set_child_count(chunk_count);
311
312            info!(
313                "target {:?}, file_size {}, blob_index {}, blob_id {}, chunk_size {}, chunk_count {}",
314                self.target(),
315                self.inode.size(),
316                external_blob_index,
317                external_blob_id,
318                external_chunk_size,
319                chunk_count
320            );
321            for i in 0..self.inode.child_count() {
322                let mut chunk = self.inode.create_chunk();
323                let file_offset = i as u64 * external_chunk_size as u64;
324                let compressed_size = if i == self.inode.child_count() - 1 {
325                    self.inode.size() - (external_chunk_size * i as u64)
326                } else {
327                    external_chunk_size
328                } as u32;
329                chunk.set_blob_index(external_blob_index);
330                chunk.set_index(external_blob_ctx.alloc_chunk_index()?);
331                chunk.set_compressed_offset(external_compressed_offset);
332                chunk.set_compressed_size(compressed_size);
333                chunk.set_uncompressed_offset(external_compressed_offset);
334                chunk.set_uncompressed_size(compressed_size);
335                chunk.set_compressed(false);
336                chunk.set_file_offset(file_offset);
337                external_compressed_offset += compressed_size as u64;
338                external_blob_ctx.chunk_size = external_chunk_size as u32;
339
340                if ctx.crc32_algorithm != crc32::Algorithm::None {
341                    self.set_external_chunk_crc32(ctx, &mut chunk, i)?
342                }
343
344                if let Some(h) = inode_hasher.as_mut() {
345                    h.digest_update(chunk.id().as_ref());
346                }
347
348                self.chunks.push(NodeChunk {
349                    source: ChunkSource::Build,
350                    inner: Arc::new(chunk),
351                });
352            }
353
354            if let Some(h) = inode_hasher {
355                self.inode.set_digest(h.digest_finalize());
356            }
357
358            return Ok(0);
359        }
360
361        // `child_count` of regular file is reused as `chunk_count`.
362        for i in 0..self.inode.child_count() {
363            let chunk_size = ctx.chunk_size;
364            let file_offset = i as u64 * chunk_size as u64;
365            let uncompressed_size = if i == self.inode.child_count() - 1 {
366                (self.inode.size() - chunk_size as u64 * i as u64) as u32
367            } else {
368                chunk_size
369            };
370
371            let chunk_data = &mut data_buf[0..uncompressed_size as usize];
372            let (mut chunk, mut chunk_info) =
373                self.read_file_chunk(ctx, reader, chunk_data, blob_mgr.external)?;
374            if let Some(h) = inode_hasher.as_mut() {
375                h.digest_update(chunk.id().as_ref());
376            }
377
378            // No need to perform chunk deduplication for tar-tarfs/external blob case.
379            if ctx.conversion_type != ConversionType::TarToTarfs && !blob_mgr.external {
380                chunk = match self.deduplicate_chunk(
381                    ctx,
382                    blob_mgr,
383                    file_offset,
384                    uncompressed_size,
385                    chunk,
386                )? {
387                    None => continue,
388                    Some(c) => c,
389                };
390            }
391
392            let (blob_index, blob_ctx) = blob_mgr.get_or_create_current_blob(ctx)?;
393            let chunk_index = blob_ctx.alloc_chunk_index()?;
394            chunk.set_blob_index(blob_index);
395            chunk.set_index(chunk_index);
396            chunk.set_file_offset(file_offset);
397            let mut dumped_size = chunk.compressed_size();
398            if ctx.conversion_type == ConversionType::TarToTarfs {
399                chunk.set_uncompressed_offset(chunk.compressed_offset());
400                chunk.set_uncompressed_size(chunk.compressed_size());
401            } else {
402                let (info, d_size) =
403                    self.dump_file_chunk(ctx, blob_ctx, blob_writer, chunk_data, &mut chunk)?;
404                if info.is_some() {
405                    chunk_info = info;
406                }
407                if let Some(d_size) = d_size {
408                    dumped_size = d_size;
409                }
410            }
411
412            let chunk = Arc::new(chunk);
413            blob_size += dumped_size as u64;
414            if ctx.conversion_type != ConversionType::TarToTarfs {
415                blob_ctx.add_chunk_meta_info(&chunk, chunk_info)?;
416                blob_mgr
417                    .layered_chunk_dict
418                    .add_chunk(chunk.clone(), ctx.digester);
419            }
420            self.chunks.push(NodeChunk {
421                source: ChunkSource::Build,
422                inner: chunk,
423            });
424        }
425
426        // Finish inode digest calculation
427        if let Some(h) = inode_hasher {
428            self.inode.set_digest(h.digest_finalize());
429        }
430
431        Ok(blob_size)
432    }
433
434    fn set_external_chunk_crc32(
435        &self,
436        ctx: &BuildContext,
437        chunk: &mut ChunkWrapper,
438        i: u32,
439    ) -> Result<()> {
440        if let Some(crcs) = ctx.attributes.get_crcs(self.target()) {
441            if (i as usize) >= crcs.len() {
442                return Err(anyhow!(
443                    "invalid crc index {} for file {}",
444                    i,
445                    self.target().display()
446                ));
447            }
448            chunk.set_has_crc32(true);
449            chunk.set_crc32(crcs[i as usize]);
450        }
451        Ok(())
452    }
453
454    fn read_file_chunk<R: Read>(
455        &self,
456        ctx: &BuildContext,
457        reader: &mut R,
458        buf: &mut [u8],
459        external: bool,
460    ) -> Result<(ChunkWrapper, Option<BlobChunkInfoV2Ondisk>)> {
461        let mut chunk = self.inode.create_chunk();
462        let mut chunk_info = None;
463        if let Some(ref zran) = ctx.blob_zran_generator {
464            let mut zran = zran.lock().unwrap();
465            zran.start_chunk(ctx.chunk_size as u64)?;
466            if !external {
467                reader
468                    .read_exact(buf)
469                    .with_context(|| format!("failed to read node file {:?}", self.path()))?;
470            }
471            let info = zran.finish_chunk()?;
472            chunk.set_compressed_offset(info.compressed_offset());
473            chunk.set_compressed_size(info.compressed_size());
474            chunk.set_compressed(true);
475            chunk_info = Some(info);
476        } else if let Some(ref tar_reader) = ctx.blob_tar_reader {
477            // For `tar-ref` case
478            let pos = tar_reader.position();
479            chunk.set_compressed_offset(pos);
480            chunk.set_compressed_size(buf.len() as u32);
481            chunk.set_compressed(false);
482            if !external {
483                reader
484                    .read_exact(buf)
485                    .with_context(|| format!("failed to read node file {:?}", self.path()))?;
486            }
487        } else if !external {
488            reader
489                .read_exact(buf)
490                .with_context(|| format!("failed to read node file {:?}", self.path()))?;
491        }
492
493        // For tar-tarfs case, no need to compute chunk id.
494        if ctx.conversion_type != ConversionType::TarToTarfs && !external {
495            chunk.set_id(RafsDigest::from_buf(buf, ctx.digester));
496            if ctx.crc32_algorithm != crc32::Algorithm::None {
497                chunk.set_has_crc32(true);
498                chunk.set_crc32(crc32::Crc32::new(ctx.crc32_algorithm).from_buf(buf));
499            }
500        }
501
502        if ctx.cipher != crypt::Algorithm::None && !external {
503            chunk.set_encrypted(true);
504        }
505
506        Ok((chunk, chunk_info))
507    }
508
509    /// Dump a chunk from u8 slice into the data blob.
510    /// Return `BlobChunkInfoV2Ondisk` iff the chunk is added into a batch chunk.
511    /// Return dumped size iff not `BlobFeatures::SEPARATE`.
512    /// Dumped size can be zero if chunk data is cached in Batch Generator,
513    /// and may contain previous chunk data cached in Batch Generator.
514    fn dump_file_chunk(
515        &self,
516        ctx: &BuildContext,
517        blob_ctx: &mut BlobContext,
518        blob_writer: &mut dyn Artifact,
519        chunk_data: &[u8],
520        chunk: &mut ChunkWrapper,
521    ) -> Result<(Option<BlobChunkInfoV2Ondisk>, Option<u32>)> {
522        let d_size = chunk_data.len() as u32;
523        let aligned_d_size = if ctx.aligned_chunk {
524            // Safe to unwrap because `chunk_size` is much less than u32::MAX.
525            try_round_up_4k(d_size).unwrap()
526        } else {
527            d_size
528        };
529        let pre_d_offset = blob_ctx.current_uncompressed_offset;
530        blob_ctx.uncompressed_blob_size = pre_d_offset + aligned_d_size as u64;
531        blob_ctx.current_uncompressed_offset += aligned_d_size as u64;
532        chunk.set_uncompressed_offset(pre_d_offset);
533        chunk.set_uncompressed_size(d_size);
534
535        let mut chunk_info = None;
536        let encrypted = blob_ctx.blob_cipher != crypt::Algorithm::None;
537        let mut dumped_size = None;
538
539        if ctx.blob_batch_generator.is_some()
540            && self.inode.child_count() == 1
541            && d_size < ctx.batch_size / 2
542        {
543            // This chunk will be added into a batch chunk.
544            let mut batch = ctx.blob_batch_generator.as_ref().unwrap().lock().unwrap();
545
546            if batch.chunk_data_buf_len() as u32 + d_size < ctx.batch_size {
547                // Add into current batch chunk directly.
548                chunk_info = Some(batch.generate_chunk_info(
549                    blob_ctx.current_compressed_offset,
550                    pre_d_offset,
551                    d_size,
552                    encrypted,
553                )?);
554                batch.append_chunk_data_buf(chunk_data);
555            } else {
556                // Dump current batch chunk if exists, and then add into a new batch chunk.
557                if !batch.chunk_data_buf_is_empty() {
558                    // Dump current batch chunk.
559                    let (_, c_size, _) =
560                        Self::write_chunk_data(ctx, blob_ctx, blob_writer, batch.chunk_data_buf())?;
561                    dumped_size = Some(c_size);
562                    batch.add_context(c_size);
563                    batch.clear_chunk_data_buf();
564                }
565
566                // Add into a new batch chunk.
567                chunk_info = Some(batch.generate_chunk_info(
568                    blob_ctx.current_compressed_offset,
569                    pre_d_offset,
570                    d_size,
571                    encrypted,
572                )?);
573                batch.append_chunk_data_buf(chunk_data);
574            }
575        } else if !ctx.blob_features.contains(BlobFeatures::SEPARATE) {
576            // For other case which needs to write chunk data to data blobs. Which means,
577            // `tar-ref`, `targz-ref`, `estargz-ref`, and `estargzindex-ref`, are excluded.
578
579            // Interrupt and dump buffered batch chunks.
580            // TODO: cancel the interruption.
581            if let Some(batch) = &ctx.blob_batch_generator {
582                let mut batch = batch.lock().unwrap();
583                if !batch.chunk_data_buf_is_empty() {
584                    // Dump current batch chunk.
585                    let (_, c_size, _) =
586                        Self::write_chunk_data(ctx, blob_ctx, blob_writer, batch.chunk_data_buf())?;
587                    dumped_size = Some(c_size);
588                    batch.add_context(c_size);
589                    batch.clear_chunk_data_buf();
590                }
591            }
592
593            let (pre_c_offset, c_size, is_compressed) =
594                Self::write_chunk_data(ctx, blob_ctx, blob_writer, chunk_data)
595                    .with_context(|| format!("failed to write chunk data {:?}", self.path()))?;
596            dumped_size = Some(dumped_size.unwrap_or(0) + c_size);
597            chunk.set_compressed_offset(pre_c_offset);
598            chunk.set_compressed_size(c_size);
599            chunk.set_compressed(is_compressed);
600        }
601
602        if let Some(blob_cache) = ctx.blob_cache_generator.as_ref() {
603            blob_cache.write_blob_data(chunk_data, chunk, aligned_d_size)?;
604        }
605        event_tracer!("blob_uncompressed_size", +d_size);
606
607        Ok((chunk_info, dumped_size))
608    }
609
610    pub fn write_chunk_data(
611        _ctx: &BuildContext,
612        blob_ctx: &mut BlobContext,
613        blob_writer: &mut dyn Artifact,
614        chunk_data: &[u8],
615    ) -> Result<(u64, u32, bool)> {
616        let (compressed, is_compressed) = compress::compress(chunk_data, blob_ctx.blob_compressor)
617            .with_context(|| "failed to compress node file".to_string())?;
618        let encrypted = crypt::encrypt_with_context(
619            &compressed,
620            &blob_ctx.cipher_object,
621            &blob_ctx.cipher_ctx,
622            blob_ctx.blob_cipher != crypt::Algorithm::None,
623        )?;
624        let compressed_size = encrypted.len() as u32;
625        let pre_compressed_offset = blob_ctx.current_compressed_offset;
626        if !blob_ctx.external {
627            // For the external blob, both compressor and encrypter should
628            // be none, and we don't write data into blob file.
629            blob_writer
630                .write_all(&encrypted)
631                .context("failed to write blob")?;
632            blob_ctx.blob_hash.update(&encrypted);
633        }
634        blob_ctx.current_compressed_offset += compressed_size as u64;
635        blob_ctx.compressed_blob_size += compressed_size as u64;
636
637        Ok((pre_compressed_offset, compressed_size, is_compressed))
638    }
639
640    fn deduplicate_chunk(
641        &mut self,
642        ctx: &BuildContext,
643        blob_mgr: &mut BlobManager,
644        file_offset: u64,
645        uncompressed_size: u32,
646        mut chunk: ChunkWrapper,
647    ) -> Result<Option<ChunkWrapper>> {
648        let dict = &blob_mgr.global_chunk_dict;
649        let mut cached_chunk = dict.get_chunk(chunk.id(), uncompressed_size);
650        let from_dict = cached_chunk.is_some();
651        if cached_chunk.is_none() {
652            cached_chunk = blob_mgr
653                .layered_chunk_dict
654                .get_chunk(chunk.id(), uncompressed_size);
655        }
656        let cached_chunk = match cached_chunk {
657            Some(v) => v,
658            None => return Ok(Some(chunk)),
659        };
660
661        // The chunks of hardlink should be always deduplicated.
662        if !self.is_hardlink() {
663            event_tracer!("dedup_uncompressed_size", +uncompressed_size);
664            event_tracer!("dedup_chunks", +1);
665        }
666        chunk.copy_from(cached_chunk);
667        chunk.set_file_offset(file_offset);
668
669        // Only add actually referenced data blobs from chunk dictionary to the blob table.
670        if from_dict {
671            let blob_index = if let Some(blob_idx) = dict.get_real_blob_idx(chunk.blob_index()) {
672                blob_idx
673            } else {
674                let blob_idx = blob_mgr.alloc_index()?;
675                dict.set_real_blob_idx(chunk.blob_index(), blob_idx);
676                if let Some(blob) = dict.get_blob_by_inner_idx(chunk.blob_index()) {
677                    let ctx = BlobContext::from(ctx, blob, ChunkSource::Dict)?;
678                    blob_mgr.add_blob(ctx);
679                }
680                blob_idx
681            };
682            chunk.set_blob_index(blob_index);
683        }
684
685        trace!(
686            "\t\tfound duplicated chunk: {} compressor {}",
687            chunk,
688            ctx.compressor
689        );
690        let source = if from_dict {
691            ChunkSource::Dict
692        } else if Some(chunk.blob_index()) != blob_mgr.get_current_blob().map(|(u, _)| u) {
693            ChunkSource::Parent
694        } else {
695            ChunkSource::Build
696        };
697        self.chunks.push(NodeChunk {
698            source,
699            inner: Arc::new(chunk),
700        });
701
702        Ok(None)
703    }
704}
705
706// build node object from a filesystem object.
707impl Node {
708    #[allow(clippy::too_many_arguments)]
709    /// Create a new instance of [Node] from a filesystem object.
710    pub fn from_fs_object(
711        version: RafsVersion,
712        source: PathBuf,
713        path: PathBuf,
714        overlay: Overlay,
715        chunk_size: u32,
716        file_size: u64,
717        explicit_uidgid: bool,
718        v6_force_extended_inode: bool,
719    ) -> Result<Node> {
720        let target = Self::generate_target(&path, &source);
721        let target_vec = Self::generate_target_vec(&target);
722        let info = NodeInfo {
723            explicit_uidgid,
724            src_ino: 0,
725            src_dev: u64::MAX,
726            rdev: u64::MAX,
727            source,
728            target,
729            path,
730            target_vec,
731            symlink: None,
732            xattrs: RafsXAttrs::default(),
733            v6_force_extended_inode,
734        };
735        let mut node = Node {
736            info: Arc::new(info),
737            index: 0,
738            layer_idx: 0,
739            overlay,
740            inode: InodeWrapper::new(version),
741            chunks: Vec::new(),
742            v6_datalayout: EROFS_INODE_FLAT_PLAIN,
743            v6_compact_inode: false,
744            v6_offset: 0,
745            v6_dirents_offset: 0,
746            v6_dirents: Vec::new(),
747        };
748
749        node.build_inode(chunk_size, file_size)
750            .context("failed to build Node from fs object")?;
751        if version.is_v6() {
752            node.v6_set_inode_compact();
753        }
754
755        Ok(node)
756    }
757
758    fn build_inode_xattr(&mut self) -> Result<()> {
759        let file_xattrs = match xattr::list(self.path()) {
760            Ok(x) => x,
761            Err(e) => {
762                if e.raw_os_error() == Some(libc::EOPNOTSUPP) {
763                    return Ok(());
764                } else {
765                    return Err(anyhow!(
766                        "failed to list xattr of {}, {}",
767                        self.path().display(),
768                        e
769                    ));
770                }
771            }
772        };
773
774        let mut info = self.info.deref().clone();
775        for key in file_xattrs {
776            let value = xattr::get(self.path(), &key).with_context(|| {
777                format!("failed to get xattr {:?} of {}", key, self.path().display())
778            })?;
779            info.xattrs.add(key, value.unwrap_or_default())?;
780        }
781        if !info.xattrs.is_empty() {
782            self.inode.set_has_xattr(true);
783        }
784        self.info = Arc::new(info);
785
786        Ok(())
787    }
788
789    fn build_inode_stat(&mut self, file_size: u64) -> Result<()> {
790        let meta = self
791            .meta()
792            .with_context(|| format!("failed to get metadata of {}", self.path().display()))?;
793        let mut info = self.info.deref().clone();
794
795        info.src_ino = meta.st_ino();
796        info.src_dev = meta.st_dev();
797        info.rdev = meta.st_rdev();
798
799        self.inode.set_mode(meta.st_mode());
800        if info.explicit_uidgid {
801            self.inode.set_uid(meta.st_uid());
802            self.inode.set_gid(meta.st_gid());
803        }
804
805        // Usually the root directory is created by the build tool (nydusify/buildkit/acceld)
806        // and the mtime of the root directory is different for each build, which makes it
807        // completely impossible to achieve repeatable builds, especially in a tar build scenario
808        // (blob + bootstrap in one tar layer), which causes the layer hash to change and wastes
809        // registry storage space, so the mtime of the root directory is forced to be ignored here.
810        let ignore_mtime = self.is_root();
811        if !ignore_mtime {
812            self.inode.set_mtime(meta.st_mtime() as u64);
813            self.inode.set_mtime_nsec(meta.st_mtime_nsec() as u32);
814        }
815        self.inode.set_projid(0);
816        self.inode.set_rdev(meta.st_rdev() as u32);
817        // Ignore actual nlink value and calculate from rootfs directory instead
818        self.inode.set_nlink(1);
819
820        // Different filesystem may have different algorithms to calculate size/blocks for
821        // directory entries, so let's ignore the value provided by source filesystem and
822        // calculate it later by ourself.
823        if !self.is_dir() {
824            // If the file size is not 0, and the meta size is 0, it means the file is an
825            // external dummy file. We need to set the size to file_size.
826            if file_size != 0 && meta.st_size() == 0 {
827                self.inode.set_size(file_size);
828            } else {
829                self.inode.set_size(meta.st_size());
830            }
831            self.v5_set_inode_blocks();
832        }
833        self.info = Arc::new(info);
834
835        Ok(())
836    }
837
838    fn build_inode(&mut self, chunk_size: u32, file_size: u64) -> Result<()> {
839        let size = self.name().byte_size();
840        if size > u16::MAX as usize {
841            bail!("file name length 0x{:x} is too big", size,);
842        }
843        self.inode.set_name_size(size);
844
845        // NOTE: Always retrieve xattr before attr so that we can know the size of xattr pairs.
846        self.build_inode_xattr()
847            .with_context(|| format!("failed to get xattr for {}", self.path().display()))?;
848        self.build_inode_stat(file_size)
849            .with_context(|| format!("failed to build inode {}", self.path().display()))?;
850
851        if self.is_reg() {
852            let chunk_count = self.chunk_count(chunk_size as u64).with_context(|| {
853                format!("failed to get chunk count for {}", self.path().display())
854            })?;
855            self.inode.set_child_count(chunk_count);
856        } else if self.is_symlink() {
857            let target_path = fs::read_link(self.path()).with_context(|| {
858                format!(
859                    "failed to read symlink target for {}",
860                    self.path().display()
861                )
862            })?;
863            let symlink: OsString = target_path.into();
864            let size = symlink.byte_size();
865            if size > u16::MAX as usize {
866                bail!("symlink content size 0x{:x} is too big", size);
867            }
868            self.inode.set_symlink_size(size);
869            self.set_symlink(symlink);
870        }
871
872        Ok(())
873    }
874
875    fn meta(&self) -> Result<impl MetadataExt> {
876        self.path()
877            .symlink_metadata()
878            .with_context(|| format!("failed to get metadata of {}", self.path().display()))
879    }
880}
881
882// Access Methods
883impl Node {
884    pub fn is_root(&self) -> bool {
885        self.target() == OsStr::from_bytes(ROOT_PATH_NAME)
886    }
887
888    pub fn is_dir(&self) -> bool {
889        self.inode.is_dir()
890    }
891
892    pub fn is_symlink(&self) -> bool {
893        self.inode.is_symlink()
894    }
895
896    pub fn is_reg(&self) -> bool {
897        self.inode.is_reg()
898    }
899
900    pub fn is_hardlink(&self) -> bool {
901        self.inode.is_hardlink()
902    }
903
904    pub fn is_special(&self) -> bool {
905        self.inode.is_special()
906    }
907
908    pub fn chunk_count(&self, chunk_size: u64) -> Result<u32> {
909        if self.is_reg() {
910            let chunks = div_round_up(self.inode.size(), chunk_size);
911            if chunks > u32::MAX as u64 {
912                bail!("file size 0x{:x} is too big", self.inode.size())
913            } else {
914                Ok(chunks as u32)
915            }
916        } else {
917            Ok(0)
918        }
919    }
920
921    /// Get file type of the inode.
922    pub fn file_type(&self) -> &str {
923        let mut file_type = "";
924
925        if self.is_symlink() {
926            file_type = "symlink";
927        } else if self.is_dir() {
928            file_type = "dir"
929        } else if self.is_reg() {
930            if self.is_hardlink() {
931                file_type = "hardlink";
932            } else {
933                file_type = "file";
934            }
935        }
936
937        file_type
938    }
939
940    /// Get filename of the inode.
941    pub fn name(&self) -> &OsStr {
942        let len = self.info.target_vec.len();
943        if len != 0 {
944            &self.info.target_vec[len - 1]
945        } else if self.path() == &self.info.source {
946            OsStr::from_bytes(ROOT_PATH_NAME)
947        } else {
948            // Safe to unwrap because `path` is returned from `path()` which is canonicalized
949            self.path().file_name().unwrap()
950        }
951    }
952
953    /// Get path of the inode
954    pub fn path(&self) -> &PathBuf {
955        &self.info.path
956    }
957
958    /// Generate cached components of the target file path.
959    pub fn generate_target_vec(target: &Path) -> Vec<OsString> {
960        target
961            .components()
962            .map(|comp| match comp {
963                Component::RootDir => OsString::from("/"),
964                Component::Normal(name) => name.to_os_string(),
965                _ => panic!("invalid file component pattern!"),
966            })
967            .collect::<Vec<_>>()
968    }
969
970    /// Get cached components of the target file path.
971    pub fn target_vec(&self) -> &[OsString] {
972        &self.info.target_vec
973    }
974
975    /// Generate target path by stripping the `root` prefix.
976    ///
977    /// Strip the `root` prefix if `path` starts with `root`, otherwise keep `path` as is.
978    /// For example:
979    /// root: /absolute/path/to/rootfs
980    /// path: /absolute/path/to/rootfs/file => /file
981    /// path /not_rootfs_prefix/file => /not_rootfs_prefix/file
982    pub fn generate_target(path: &Path, root: &Path) -> PathBuf {
983        if let Ok(p) = path.strip_prefix(root) {
984            Path::new("/").join(p)
985        } else {
986            // Compatible with path `/`
987            path.to_path_buf()
988        }
989    }
990
991    /// Get the absolute path of the inode within the RAFS filesystem.
992    pub fn target(&self) -> &PathBuf {
993        &self.info.target
994    }
995
996    /// Set symlink target for the node.
997    pub fn set_symlink(&mut self, symlink: OsString) {
998        let mut info = self.info.deref().clone();
999        info.symlink = Some(symlink);
1000        self.info = Arc::new(info);
1001    }
1002
1003    /// Set extended attributes for the node.
1004    pub fn set_xattr(&mut self, xattr: RafsXAttrs) {
1005        let mut info = self.info.deref().clone();
1006        info.xattrs = xattr;
1007        self.info = Arc::new(info);
1008    }
1009
1010    /// Delete an extend attribute with id `key`.
1011    pub fn remove_xattr(&mut self, key: &OsStr) {
1012        let mut info = self.info.deref().clone();
1013        info.xattrs.remove(key);
1014        if info.xattrs.is_empty() {
1015            self.inode.set_has_xattr(false);
1016        }
1017        self.info = Arc::new(info);
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use std::{collections::HashMap, io::BufReader};
1024
1025    use nydus_utils::{digest, BufReaderInfo};
1026    use vmm_sys_util::tempfile::TempFile;
1027
1028    use crate::{attributes::Attributes, ArtifactWriter, BlobCacheGenerator, HashChunkDict};
1029
1030    use super::*;
1031
1032    #[test]
1033    fn test_node_chunk() {
1034        let chunk_wrapper1 = ChunkWrapper::new(RafsVersion::V5);
1035        let mut chunk = NodeChunk {
1036            source: ChunkSource::Build,
1037            inner: Arc::new(chunk_wrapper1),
1038        };
1039        println!("NodeChunk: {}", chunk);
1040        matches!(chunk.inner.deref().clone(), ChunkWrapper::V5(_));
1041
1042        let chunk_wrapper2 = ChunkWrapper::new(RafsVersion::V6);
1043        chunk.copy_from(&chunk_wrapper2);
1044        matches!(chunk.inner.deref().clone(), ChunkWrapper::V6(_));
1045
1046        chunk.set_index(0x10);
1047        assert_eq!(chunk.inner.index(), 0x10);
1048        chunk.set_blob_index(0x20);
1049        assert_eq!(chunk.inner.blob_index(), 0x20);
1050        chunk.set_compressed_size(0x30);
1051        assert_eq!(chunk.inner.compressed_size(), 0x30);
1052        chunk.set_file_offset(0x40);
1053        assert_eq!(chunk.inner.file_offset(), 0x40);
1054    }
1055
1056    #[test]
1057    fn test_node_dump_node_data() {
1058        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
1059        let mut source_path = PathBuf::from(root_dir);
1060        source_path.push("../tests/texture/blobs/be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef");
1061
1062        let mut inode = InodeWrapper::new(RafsVersion::V5);
1063        inode.set_child_count(2);
1064        inode.set_size(20);
1065        let info = NodeInfo {
1066            explicit_uidgid: true,
1067            src_ino: 1,
1068            src_dev: u64::MAX,
1069            rdev: u64::MAX,
1070            path: source_path.clone(),
1071            source: PathBuf::from("/"),
1072            target: source_path.clone(),
1073            target_vec: vec![OsString::from(source_path)],
1074            symlink: Some(OsString::from("symlink")),
1075            xattrs: RafsXAttrs::new(),
1076            v6_force_extended_inode: false,
1077        };
1078        let mut node = Node::new(inode, info, 1);
1079
1080        let mut ctx = BuildContext::default();
1081        ctx.set_chunk_size(2);
1082        ctx.conversion_type = ConversionType::TarToRef;
1083        ctx.cipher = crypt::Algorithm::Aes128Xts;
1084        let tmp_file1 = TempFile::new().unwrap();
1085        std::fs::write(
1086            tmp_file1.as_path(),
1087            "This is a test!\n".repeat(32).as_bytes(),
1088        )
1089        .unwrap();
1090        let buf_reader = BufReader::new(tmp_file1.into_file());
1091        ctx.blob_tar_reader = Some(BufReaderInfo::from_buf_reader(buf_reader));
1092        let tmp_file2 = TempFile::new().unwrap();
1093        ctx.blob_cache_generator = Some(
1094            BlobCacheGenerator::new(crate::ArtifactStorage::SingleFile(PathBuf::from(
1095                tmp_file2.as_path(),
1096            )))
1097            .unwrap(),
1098        );
1099
1100        let mut blob_mgr = BlobManager::new(digest::Algorithm::Sha256, false);
1101        let mut chunk_dict = HashChunkDict::new(digest::Algorithm::Sha256);
1102        let mut chunk_wrapper = ChunkWrapper::new(RafsVersion::V5);
1103        chunk_wrapper.set_id(RafsDigest {
1104            data: [
1105                209, 217, 144, 116, 135, 113, 3, 121, 133, 92, 96, 25, 219, 145, 151, 219, 119, 47,
1106                96, 147, 90, 51, 78, 44, 193, 149, 6, 102, 13, 173, 138, 191,
1107            ],
1108        });
1109        chunk_wrapper.set_uncompressed_size(2);
1110        chunk_dict.add_chunk(Arc::new(chunk_wrapper), digest::Algorithm::Sha256);
1111        blob_mgr.set_chunk_dict(Arc::new(chunk_dict));
1112
1113        let tmp_file3 = TempFile::new().unwrap();
1114        let mut blob_writer = ArtifactWriter::new(crate::ArtifactStorage::SingleFile(
1115            PathBuf::from(tmp_file3.as_path()),
1116        ))
1117        .unwrap();
1118
1119        let mut chunk_data_buf = [1u8; 32];
1120
1121        node.inode.set_mode(0o755 | libc::S_IFDIR as u32);
1122        let data_size =
1123            node.dump_node_data(&ctx, &mut blob_mgr, &mut blob_writer, &mut chunk_data_buf);
1124        assert!(data_size.is_ok());
1125        assert_eq!(data_size.unwrap(), 0);
1126
1127        node.inode.set_mode(0o755 | libc::S_IFLNK as u32);
1128        let data_size =
1129            node.dump_node_data(&ctx, &mut blob_mgr, &mut blob_writer, &mut chunk_data_buf);
1130        assert!(data_size.is_ok());
1131        assert_eq!(data_size.unwrap(), 0);
1132
1133        node.inode.set_mode(0o755 | libc::S_IFBLK as u32);
1134        let data_size =
1135            node.dump_node_data(&ctx, &mut blob_mgr, &mut blob_writer, &mut chunk_data_buf);
1136        assert!(data_size.is_ok());
1137        assert_eq!(data_size.unwrap(), 0);
1138
1139        node.inode.set_mode(0o755 | libc::S_IFREG as u32);
1140        let data_size =
1141            node.dump_node_data(&ctx, &mut blob_mgr, &mut blob_writer, &mut chunk_data_buf);
1142        assert!(data_size.is_ok());
1143        assert_eq!(data_size.unwrap(), 18);
1144    }
1145
1146    #[test]
1147    fn test_node() {
1148        let inode = InodeWrapper::new(RafsVersion::V5);
1149        let info = NodeInfo {
1150            explicit_uidgid: true,
1151            src_ino: 1,
1152            src_dev: u64::MAX,
1153            rdev: u64::MAX,
1154            path: PathBuf::new(),
1155            source: PathBuf::new(),
1156            target: PathBuf::new(),
1157            target_vec: vec![OsString::new()],
1158            symlink: None,
1159            xattrs: RafsXAttrs::new(),
1160            v6_force_extended_inode: false,
1161        };
1162
1163        let mut inode1 = inode.clone();
1164        inode1.set_size(1 << 60);
1165        inode1.set_mode(0o755 | libc::S_IFREG as u32);
1166        let node = Node::new(inode1, info.clone(), 1);
1167        assert!(node.chunk_count(2).is_err());
1168
1169        let mut inode2 = inode.clone();
1170        inode2.set_mode(0o755 | libc::S_IFCHR as u32);
1171        let node = Node::new(inode2, info.clone(), 1);
1172        assert!(node.chunk_count(2).is_ok());
1173        assert_eq!(node.chunk_count(2).unwrap(), 0);
1174
1175        let mut inode3 = inode.clone();
1176        inode3.set_mode(0o755 | libc::S_IFLNK as u32);
1177        let node = Node::new(inode3, info.clone(), 1);
1178        assert_eq!(node.file_type(), "symlink");
1179        let mut inode4 = inode.clone();
1180        inode4.set_mode(0o755 | libc::S_IFDIR as u32);
1181        let node = Node::new(inode4, info.clone(), 1);
1182        assert_eq!(node.file_type(), "dir");
1183        let mut inode5 = inode.clone();
1184        inode5.set_mode(0o755 | libc::S_IFREG as u32);
1185        let node = Node::new(inode5, info.clone(), 1);
1186        assert_eq!(node.file_type(), "file");
1187
1188        let mut info1 = info.clone();
1189        info1.target_vec = vec![OsString::from("1"), OsString::from("2")];
1190        let node = Node::new(inode.clone(), info1, 1);
1191        assert_eq!(node.name(), OsString::from("2").as_os_str());
1192        let mut info2 = info.clone();
1193        info2.target_vec = vec![];
1194        info2.path = PathBuf::from("/");
1195        info2.source = PathBuf::from("/");
1196        let node = Node::new(inode.clone(), info2, 1);
1197        assert_eq!(node.name(), OsStr::from_bytes(ROOT_PATH_NAME));
1198        let mut info3 = info.clone();
1199        info3.target_vec = vec![];
1200        info3.path = PathBuf::from("/1");
1201        info3.source = PathBuf::from("/11");
1202        let node = Node::new(inode.clone(), info3, 1);
1203        assert_eq!(node.name(), OsStr::new("1"));
1204
1205        let target = PathBuf::from("/root/child");
1206        assert_eq!(
1207            Node::generate_target_vec(&target),
1208            vec![
1209                OsString::from("/"),
1210                OsString::from("root"),
1211                OsString::from("child")
1212            ]
1213        );
1214
1215        let mut node = Node::new(inode, info, 1);
1216        node.set_symlink(OsString::from("symlink"));
1217        assert_eq!(node.info.deref().symlink, Some(OsString::from("symlink")));
1218
1219        let mut xatter = RafsXAttrs::new();
1220        assert!(xatter
1221            .add(OsString::from("user.key"), [1u8; 16].to_vec())
1222            .is_ok());
1223        assert!(xatter
1224            .add(
1225                OsString::from("system.posix_acl_default.key"),
1226                [2u8; 8].to_vec()
1227            )
1228            .is_ok());
1229        node.set_xattr(xatter);
1230        node.inode.set_has_xattr(true);
1231        node.remove_xattr(OsStr::new("user.key"));
1232        assert!(node.inode.has_xattr());
1233        node.remove_xattr(OsStr::new("system.posix_acl_default.key"));
1234        assert!(!node.inode.has_xattr());
1235    }
1236
1237    #[test]
1238    fn test_set_external_chunk_crc32() {
1239        let mut ctx = BuildContext {
1240            crc32_algorithm: crc32::Algorithm::Crc32Iscsi,
1241            attributes: Attributes {
1242                crcs: HashMap::new(),
1243                ..Default::default()
1244            },
1245            ..Default::default()
1246        };
1247        let target = PathBuf::from("/test_file");
1248        ctx.attributes
1249            .crcs
1250            .insert(target.clone(), vec![0x12345678, 0x87654321]);
1251
1252        let node = Node::new(
1253            InodeWrapper::new(RafsVersion::V5),
1254            NodeInfo {
1255                path: target.clone(),
1256                target: target.clone(),
1257                ..Default::default()
1258            },
1259            1,
1260        );
1261
1262        let mut chunk = node.inode.create_chunk();
1263        print!("target: {}", node.target().display());
1264        let result = node.set_external_chunk_crc32(&ctx, &mut chunk, 1);
1265        assert!(result.is_ok());
1266        assert_eq!(chunk.crc32(), 0x87654321);
1267        assert!(chunk.has_crc32());
1268
1269        // test invalid crc index
1270        let result = node.set_external_chunk_crc32(&ctx, &mut chunk, 2);
1271        assert!(result.is_err());
1272        let err = result.unwrap_err().to_string();
1273        assert!(err.contains("invalid crc index 2 for file /test_file"));
1274    }
1275}