nydus_builder/core/
overlay.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
6//! Execute file/directory whiteout rules when merging multiple RAFS filesystems
7//! according to the OCI or Overlayfs specifications.
8
9use std::ffi::{OsStr, OsString};
10use std::fmt::{self, Display, Formatter};
11use std::os::unix::ffi::OsStrExt;
12use std::str::FromStr;
13
14use anyhow::{anyhow, Error, Result};
15
16use super::node::Node;
17
18/// Prefix for OCI whiteout file.
19pub const OCISPEC_WHITEOUT_PREFIX: &str = ".wh.";
20/// Prefix for OCI whiteout opaque.
21pub const OCISPEC_WHITEOUT_OPAQUE: &str = ".wh..wh..opq";
22/// Extended attribute key for Overlayfs whiteout opaque.
23pub const OVERLAYFS_WHITEOUT_OPAQUE: &str = "trusted.overlay.opaque";
24
25/// RAFS filesystem overlay specifications.
26///
27/// When merging multiple RAFS filesystems into one, special rules are needed to white out
28/// files/directories in lower/parent filesystems. The whiteout specification defined by the
29/// OCI image specification and Linux Overlayfs are widely adopted, so both of them are supported
30/// by RAFS filesystem.
31///
32/// # Overlayfs Whiteout
33///
34/// In order to support rm and rmdir without changing the lower filesystem, an overlay filesystem
35/// needs to record in the upper filesystem that files have been removed. This is done using
36/// whiteouts and opaque directories (non-directories are always opaque).
37///
38/// A whiteout is created as a character device with 0/0 device number. When a whiteout is found
39/// in the upper level of a merged directory, any matching name in the lower level is ignored,
40/// and the whiteout itself is also hidden.
41///
42/// A directory is made opaque by setting the xattr “trusted.overlay.opaque” to “y”. Where the upper
43/// filesystem contains an opaque directory, any directory in the lower filesystem with the same
44/// name is ignored.
45///
46/// # OCI Image Whiteout
47/// - A whiteout file is an empty file with a special filename that signifies a path should be
48///   deleted.
49/// - A whiteout filename consists of the prefix .wh. plus the basename of the path to be deleted.
50/// - As files prefixed with .wh. are special whiteout markers, it is not possible to create a
51///   filesystem which has a file or directory with a name beginning with .wh..
52/// - Once a whiteout is applied, the whiteout itself MUST also be hidden.
53/// - Whiteout files MUST only apply to resources in lower/parent layers.
54/// - Files that are present in the same layer as a whiteout file can only be hidden by whiteout
55///   files in subsequent layers.
56/// - In addition to expressing that a single entry should be removed from a lower layer, layers
57///   may remove all of the children using an opaque whiteout entry.
58/// - An opaque whiteout entry is a file with the name .wh..wh..opq indicating that all siblings
59///   are hidden in the lower layer.
60#[derive(Clone, Copy, PartialEq)]
61pub enum WhiteoutSpec {
62    /// Overlay whiteout rules according to the OCI image specification.
63    ///
64    /// https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
65    Oci,
66    /// Overlay whiteout rules according to the Linux Overlayfs specification.
67    ///
68    /// "whiteouts and opaque directories" in https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
69    Overlayfs,
70    /// No whiteout, keep all content from lower/parent filesystems.
71    None,
72}
73
74impl fmt::Display for WhiteoutSpec {
75    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76        match self {
77            WhiteoutSpec::Oci => write!(f, "oci"),
78            WhiteoutSpec::Overlayfs => write!(f, "overlayfs"),
79            WhiteoutSpec::None => write!(f, "none"),
80        }
81    }
82}
83
84impl Default for WhiteoutSpec {
85    fn default() -> Self {
86        Self::Oci
87    }
88}
89
90impl FromStr for WhiteoutSpec {
91    type Err = Error;
92
93    fn from_str(s: &str) -> Result<Self> {
94        match s.to_lowercase().as_str() {
95            "oci" => Ok(Self::Oci),
96            "overlayfs" => Ok(Self::Overlayfs),
97            "none" => Ok(Self::None),
98            _ => Err(anyhow!("invalid whiteout spec")),
99        }
100    }
101}
102
103/// RAFS filesystem overlay operation types.
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub enum WhiteoutType {
106    OciOpaque,
107    OciRemoval,
108    OverlayFsOpaque,
109    OverlayFsRemoval,
110}
111
112impl WhiteoutType {
113    pub fn is_removal(&self) -> bool {
114        *self == WhiteoutType::OciRemoval || *self == WhiteoutType::OverlayFsRemoval
115    }
116}
117
118/// RAFS filesystem node overlay state.
119#[allow(dead_code)]
120#[derive(Clone, Debug, PartialEq)]
121pub enum Overlay {
122    Lower,
123    UpperAddition,
124    UpperModification,
125}
126
127impl Overlay {
128    pub fn is_lower_layer(&self) -> bool {
129        self == &Overlay::Lower
130    }
131}
132
133impl Display for Overlay {
134    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
135        match self {
136            Overlay::Lower => write!(f, "LOWER"),
137            Overlay::UpperAddition => write!(f, "ADDED"),
138            Overlay::UpperModification => write!(f, "MODIFIED"),
139        }
140    }
141}
142
143impl Node {
144    /// Check whether the inode is a special overlayfs whiteout file.
145    pub fn is_overlayfs_whiteout(&self, spec: WhiteoutSpec) -> bool {
146        if spec != WhiteoutSpec::Overlayfs {
147            return false;
148        }
149        self.inode.is_chrdev()
150            && nydus_utils::compact::major_dev(self.info.rdev) == 0
151            && nydus_utils::compact::minor_dev(self.info.rdev) == 0
152    }
153
154    /// Check whether the inode (directory) is a overlayfs whiteout opaque.
155    pub fn is_overlayfs_opaque(&self, spec: WhiteoutSpec) -> bool {
156        if spec != WhiteoutSpec::Overlayfs || !self.is_dir() {
157            return false;
158        }
159
160        // A directory is made opaque by setting the xattr "trusted.overlay.opaque" to "y".
161        if let Some(v) = self
162            .info
163            .xattrs
164            .get(&OsString::from(OVERLAYFS_WHITEOUT_OPAQUE))
165        {
166            if let Ok(v) = std::str::from_utf8(v.as_slice()) {
167                return v == "y";
168            }
169        }
170
171        false
172    }
173
174    /// Get whiteout type to process the inode.
175    pub fn whiteout_type(&self, spec: WhiteoutSpec) -> Option<WhiteoutType> {
176        if self.overlay == Overlay::Lower {
177            return None;
178        }
179
180        match spec {
181            WhiteoutSpec::Oci => {
182                if let Some(name) = self.name().to_str() {
183                    if name == OCISPEC_WHITEOUT_OPAQUE {
184                        return Some(WhiteoutType::OciOpaque);
185                    } else if name.starts_with(OCISPEC_WHITEOUT_PREFIX) {
186                        return Some(WhiteoutType::OciRemoval);
187                    }
188                }
189            }
190            WhiteoutSpec::Overlayfs => {
191                if self.is_overlayfs_whiteout(spec) {
192                    return Some(WhiteoutType::OverlayFsRemoval);
193                } else if self.is_overlayfs_opaque(spec) {
194                    return Some(WhiteoutType::OverlayFsOpaque);
195                }
196            }
197            WhiteoutSpec::None => {
198                return None;
199            }
200        }
201
202        None
203    }
204
205    /// Get original filename from a whiteout filename.
206    pub fn origin_name(&self, t: WhiteoutType) -> Option<&OsStr> {
207        if let Some(name) = self.name().to_str() {
208            if t == WhiteoutType::OciRemoval {
209                // the whiteout filename prefixes the basename of the path to be deleted with ".wh.".
210                return Some(OsStr::from_bytes(
211                    name[OCISPEC_WHITEOUT_PREFIX.len()..].as_bytes(),
212                ));
213            } else if t == WhiteoutType::OverlayFsRemoval {
214                // the whiteout file has the same name as the file to be deleted.
215                return Some(name.as_ref());
216            }
217        }
218
219        None
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use nydus_rafs::metadata::{inode::InodeWrapper, layout::v5::RafsV5Inode};
226
227    use crate::core::node::NodeInfo;
228
229    use super::*;
230
231    #[test]
232    fn test_white_spec_from_str() {
233        let spec = WhiteoutSpec::default();
234        assert!(matches!(spec, WhiteoutSpec::Oci));
235
236        assert!(WhiteoutSpec::from_str("oci").is_ok());
237        assert!(WhiteoutSpec::from_str("overlayfs").is_ok());
238        assert!(WhiteoutSpec::from_str("none").is_ok());
239        assert!(WhiteoutSpec::from_str("foo").is_err());
240    }
241
242    #[test]
243    fn test_white_type_removal_check() {
244        let t1 = WhiteoutType::OciOpaque;
245        let t2 = WhiteoutType::OciRemoval;
246        let t3 = WhiteoutType::OverlayFsOpaque;
247        let t4 = WhiteoutType::OverlayFsRemoval;
248        assert!(!t1.is_removal());
249        assert!(t2.is_removal());
250        assert!(!t3.is_removal());
251        assert!(t4.is_removal());
252    }
253
254    #[test]
255    fn test_overlay_low_layer_check() {
256        let t1 = Overlay::Lower;
257        let t2 = Overlay::UpperAddition;
258        let t3 = Overlay::UpperModification;
259
260        assert!(t1.is_lower_layer());
261        assert!(!t2.is_lower_layer());
262        assert!(!t3.is_lower_layer());
263    }
264
265    #[test]
266    fn test_node() {
267        let mut inode = InodeWrapper::V5(RafsV5Inode::default());
268        inode.set_mode(libc::S_IFCHR as u32);
269        let node = Node::new(inode, NodeInfo::default(), 0);
270        assert!(!node.is_overlayfs_whiteout(WhiteoutSpec::None));
271        assert!(node.is_overlayfs_whiteout(WhiteoutSpec::Overlayfs));
272        assert_eq!(
273            node.whiteout_type(WhiteoutSpec::Overlayfs).unwrap(),
274            WhiteoutType::OverlayFsRemoval
275        );
276
277        let mut inode = InodeWrapper::V5(RafsV5Inode::default());
278        let mut info: NodeInfo = NodeInfo::default();
279        assert!(info
280            .xattrs
281            .add(OVERLAYFS_WHITEOUT_OPAQUE.into(), "y".into())
282            .is_ok());
283        inode.set_mode(libc::S_IFDIR as u32);
284        let node = Node::new(inode, info, 0);
285        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::None));
286        assert!(node.is_overlayfs_opaque(WhiteoutSpec::Overlayfs));
287        assert_eq!(
288            node.whiteout_type(WhiteoutSpec::Overlayfs).unwrap(),
289            WhiteoutType::OverlayFsOpaque
290        );
291
292        let mut inode = InodeWrapper::V5(RafsV5Inode::default());
293        let mut info = NodeInfo::default();
294        assert!(info
295            .xattrs
296            .add(OVERLAYFS_WHITEOUT_OPAQUE.into(), "n".into())
297            .is_ok());
298        inode.set_mode(libc::S_IFDIR as u32);
299        let node = Node::new(inode, info, 0);
300        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::None));
301        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::Overlayfs));
302
303        let mut inode = InodeWrapper::V5(RafsV5Inode::default());
304        let mut info = NodeInfo::default();
305        assert!(info
306            .xattrs
307            .add(OVERLAYFS_WHITEOUT_OPAQUE.into(), "y".into())
308            .is_ok());
309        inode.set_mode(libc::S_IFCHR as u32);
310        let node = Node::new(inode, info, 0);
311        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::None));
312        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::Overlayfs));
313
314        let mut inode = InodeWrapper::V5(RafsV5Inode::default());
315        let mut info = NodeInfo::default();
316        assert!(info
317            .xattrs
318            .add(OVERLAYFS_WHITEOUT_OPAQUE.into(), "n".into())
319            .is_ok());
320        inode.set_mode(libc::S_IFDIR as u32);
321        let node = Node::new(inode, info, 0);
322        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::None));
323        assert!(!node.is_overlayfs_opaque(WhiteoutSpec::Overlayfs));
324
325        let inode = InodeWrapper::V5(RafsV5Inode::default());
326        let info = NodeInfo::default();
327        let mut node = Node::new(inode, info, 0);
328
329        assert_eq!(node.whiteout_type(WhiteoutSpec::None), None);
330        assert_eq!(node.whiteout_type(WhiteoutSpec::Oci), None);
331        assert_eq!(node.whiteout_type(WhiteoutSpec::Overlayfs), None);
332
333        node.overlay = Overlay::Lower;
334        assert_eq!(node.whiteout_type(WhiteoutSpec::Overlayfs), None);
335
336        let inode = InodeWrapper::V5(RafsV5Inode::default());
337        let mut info = NodeInfo::default();
338        let name = OCISPEC_WHITEOUT_PREFIX.to_string() + "foo";
339        info.target_vec.push(name.clone().into());
340        let node = Node::new(inode, info, 0);
341        assert_eq!(
342            node.whiteout_type(WhiteoutSpec::Oci).unwrap(),
343            WhiteoutType::OciRemoval
344        );
345        assert_eq!(node.origin_name(WhiteoutType::OciRemoval).unwrap(), "foo");
346        assert_eq!(node.origin_name(WhiteoutType::OciOpaque), None);
347        assert_eq!(
348            node.origin_name(WhiteoutType::OverlayFsRemoval).unwrap(),
349            OsStr::new(&name)
350        );
351
352        let inode = InodeWrapper::V5(RafsV5Inode::default());
353        let mut info = NodeInfo::default();
354        info.target_vec.push(OCISPEC_WHITEOUT_OPAQUE.into());
355        let node = Node::new(inode, info, 0);
356        assert_eq!(
357            node.whiteout_type(WhiteoutSpec::Oci).unwrap(),
358            WhiteoutType::OciOpaque
359        );
360    }
361}