libfuse_fs/util/
whiteout.rs1use std::ffi::{OsStr, OsString};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum WhiteoutFormat {
17 CharDev,
18 OciWhiteout,
19}
20
21impl Default for WhiteoutFormat {
22 fn default() -> Self {
25 if cfg!(target_os = "macos") {
26 WhiteoutFormat::OciWhiteout
27 } else {
28 WhiteoutFormat::CharDev
29 }
30 }
31}
32
33pub const OCI_WHITEOUT_PREFIX: &str = ".wh.";
35
36pub const OCI_OPAQUE_MARKER: &str = ".wh..wh..opq";
39
40pub fn is_oci_whiteout_name(name: &OsStr) -> bool {
43 let bytes = name.as_encoded_bytes();
44 bytes.starts_with(OCI_WHITEOUT_PREFIX.as_bytes()) && bytes != OCI_OPAQUE_MARKER.as_bytes()
45}
46
47pub fn is_oci_opaque_marker(name: &OsStr) -> bool {
49 name.as_encoded_bytes() == OCI_OPAQUE_MARKER.as_bytes()
50}
51
52pub fn oci_whiteout_target(raw: &OsStr) -> Option<&OsStr> {
55 if !is_oci_whiteout_name(raw) {
56 return None;
57 }
58 let bytes = raw.as_encoded_bytes();
59 let target = &bytes[OCI_WHITEOUT_PREFIX.len()..];
60 Some(unsafe { OsStr::from_encoded_bytes_unchecked(target) })
63}
64
65pub fn oci_whiteout_name(base: &OsStr) -> OsString {
67 use std::os::unix::ffi::OsStringExt;
68 let mut bytes = OCI_WHITEOUT_PREFIX.as_bytes().to_vec();
69 bytes.extend_from_slice(base.as_encoded_bytes());
70 OsString::from_vec(bytes)
71}
72
73pub fn is_user_creatable_name(format: WhiteoutFormat, name: &OsStr) -> bool {
77 match format {
78 WhiteoutFormat::CharDev => true,
79 WhiteoutFormat::OciWhiteout => {
80 let bytes = name.as_encoded_bytes();
81 !bytes.starts_with(OCI_WHITEOUT_PREFIX.as_bytes())
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn detects_whiteout_names() {
92 assert!(is_oci_whiteout_name(OsStr::new(".wh.foo")));
93 assert!(is_oci_whiteout_name(OsStr::new(".wh..hidden")));
94 assert!(!is_oci_whiteout_name(OsStr::new("foo")));
95 assert!(!is_oci_whiteout_name(OsStr::new(OCI_OPAQUE_MARKER)));
97 }
98
99 #[test]
100 fn detects_opaque_marker() {
101 assert!(is_oci_opaque_marker(OsStr::new(OCI_OPAQUE_MARKER)));
102 assert!(!is_oci_opaque_marker(OsStr::new(".wh.foo")));
103 }
104
105 #[test]
106 fn target_extraction() {
107 assert_eq!(
108 oci_whiteout_target(OsStr::new(".wh.foo")),
109 Some(OsStr::new("foo"))
110 );
111 assert_eq!(oci_whiteout_target(OsStr::new("foo")), None);
112 assert_eq!(oci_whiteout_target(OsStr::new(OCI_OPAQUE_MARKER)), None);
113 }
114
115 #[test]
116 fn name_construction_roundtrips() {
117 let made = oci_whiteout_name(OsStr::new("payload"));
118 assert_eq!(made, OsString::from(".wh.payload"));
119 assert_eq!(oci_whiteout_target(&made), Some(OsStr::new("payload")));
120 }
121
122 #[test]
123 fn user_creation_rejected_in_oci() {
124 assert!(!is_user_creatable_name(
125 WhiteoutFormat::OciWhiteout,
126 OsStr::new(".wh.evil"),
127 ));
128 assert!(is_user_creatable_name(
129 WhiteoutFormat::OciWhiteout,
130 OsStr::new("normal"),
131 ));
132 assert!(is_user_creatable_name(
134 WhiteoutFormat::CharDev,
135 OsStr::new(".wh.allowed"),
136 ));
137 }
138
139 #[test]
140 fn default_per_platform() {
141 let want = if cfg!(target_os = "macos") {
142 WhiteoutFormat::OciWhiteout
143 } else {
144 WhiteoutFormat::CharDev
145 };
146 assert_eq!(WhiteoutFormat::default(), want);
147 }
148}