1use crate::ResourceLocation;
3use std::path::{Component, Path, PathBuf};
4use std::sync::Arc;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub struct EntryName(PathBuf);
9#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10#[error("a directory entry must be one nonempty native name, without NUL or a parent component")]
11pub struct EntryNameError;
12impl EntryName {
13 pub fn new(name: PathBuf) -> Result<Self, EntryNameError> {
14 let mut components = name.components();
15 if !matches!(components.next(), Some(Component::Normal(_)))
16 || components.next().is_some()
17 || name.file_name() != Some(name.as_os_str())
18 || name.as_os_str().as_encoded_bytes().contains(&0)
19 {
20 return Err(EntryNameError);
21 }
22 Ok(Self(name))
23 }
24 pub fn as_path(&self) -> &Path {
25 &self.0
26 }
27 pub fn display(&self) -> String {
28 display_path(&self.0)
29 }
30}
31impl serde::Serialize for EntryName {
32 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
33 strop_core::path_serde::serialize(&self.0, serializer)
34 }
35}
36impl<'de> serde::Deserialize<'de> for EntryName {
37 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
38 Self::new(strop_core::path_serde::deserialize(deserializer)?)
39 .map_err(serde::de::Error::custom)
40 }
41}
42
43pub fn display_path(path: &Path) -> String {
46 use std::fmt::Write as _;
47 let mut out = String::new();
48 for chunk in path.as_os_str().as_encoded_bytes().utf8_chunks() {
49 for character in chunk.valid().chars() {
50 match character {
51 '\\' => out.push_str("\\\\"),
52 '\n' => out.push_str("\\n"),
53 '\r' => out.push_str("\\r"),
54 '\t' => out.push_str("\\t"),
55 value
56 if value.is_control()
57 || matches!(value, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}') =>
58 {
59 let _ = write!(out, "\\u{{{:X}}}", value as u32);
60 }
61 value => out.push(value),
62 }
63 }
64 for byte in chunk.invalid() {
65 let _ = write!(out, "\\x{byte:02X}");
66 }
67 }
68 out
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
72pub enum EntryKind {
73 File,
74 Directory,
75 SymbolicLink,
76 Fifo,
77 Socket,
78 BlockDevice,
79 CharacterDevice,
80 Unknown,
81}
82impl EntryKind {
83 pub const fn marker(self) -> char {
84 match self {
85 Self::File => '-',
86 Self::Directory => 'd',
87 Self::SymbolicLink => 'l',
88 Self::Fifo => 'p',
89 Self::Socket => 's',
90 Self::BlockDevice => 'b',
91 Self::CharacterDevice => 'c',
92 Self::Unknown => '?',
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
98#[serde(try_from = "u16", into = "u16")]
99pub struct Permissions(u16);
100#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
101#[error("permission bits exceed POSIX access and special bits")]
102pub struct PermissionBitsError;
103impl Permissions {
104 pub const fn new(bits: u16) -> Result<Self, PermissionBitsError> {
105 if bits & !0o7777 != 0 {
106 Err(PermissionBitsError)
107 } else {
108 Ok(Self(bits))
109 }
110 }
111 pub const fn from_mode(mode: u32) -> Self {
112 Self((mode & 0o7777) as u16)
113 }
114 pub const fn bits(self) -> u16 {
115 self.0
116 }
117}
118impl TryFrom<u16> for Permissions {
119 type Error = PermissionBitsError;
120 fn try_from(bits: u16) -> Result<Self, Self::Error> {
121 Self::new(bits)
122 }
123}
124impl From<Permissions> for u16 {
125 fn from(value: Permissions) -> Self {
126 value.bits()
127 }
128}
129impl std::fmt::Display for Permissions {
130 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 for (read, write, execute, special, lower, upper) in [
132 (0o400, 0o200, 0o100, 0o4000, 's', 'S'),
133 (0o040, 0o020, 0o010, 0o2000, 's', 'S'),
134 (0o004, 0o002, 0o001, 0o1000, 't', 'T'),
135 ] {
136 let r = if self.0 & read != 0 { 'r' } else { '-' };
137 let w = if self.0 & write != 0 { 'w' } else { '-' };
138 let x = match (self.0 & execute != 0, self.0 & special != 0) {
139 (true, true) => lower,
140 (false, true) => upper,
141 (true, false) => 'x',
142 (false, false) => '-',
143 };
144 write!(formatter, "{r}{w}{x}")?;
145 }
146 Ok(())
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
152pub struct ObjectId {
153 pub device: u64,
154 pub inode: u64,
155}
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
157pub struct FileTime {
158 pub seconds: i64,
159 pub nanos: u32,
160}
161#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
162pub struct Observation {
163 pub kind: EntryKind,
164 pub identity: Option<ObjectId>,
165 pub size: Option<u64>,
166 pub modified: Option<FileTime>,
167 pub changed: Option<FileTime>,
168 pub permissions: Option<Permissions>,
169 pub uid: Option<u32>,
170 pub gid: Option<u32>,
171 pub links: Option<u64>,
172 pub digest: Option<[u8; 32]>,
173}
174impl Observation {
175 pub fn unknown(kind: EntryKind) -> Self {
176 Self {
177 kind,
178 identity: None,
179 size: None,
180 modified: None,
181 changed: None,
182 permissions: None,
183 uid: None,
184 gid: None,
185 links: None,
186 digest: None,
187 }
188 }
189 pub fn same_object(&self, other: &Self) -> bool {
190 self.identity.is_some() && self.identity == other.identity && self.kind == other.kind
191 }
192 pub fn same_metadata(&self, other: &Self) -> bool {
194 self.kind == other.kind
195 && self.identity == other.identity
196 && self.size == other.size
197 && self.modified == other.modified
198 && self.changed == other.changed
199 && self.permissions == other.permissions
200 && self.uid == other.uid
201 && self.gid == other.gid
202 && self.links == other.links
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
207pub struct DirectoryEntry {
208 pub name: EntryName,
209 pub observation: Observation,
210 pub error: Option<String>,
211}
212#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
213pub enum ListingState {
214 Complete,
215 Limited { limit: usize },
216 Failed { message: String },
217}
218impl ListingState {
219 pub fn is_complete(&self) -> bool {
220 matches!(self, Self::Complete)
221 }
222}
223#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
224pub struct DirectorySnapshot {
225 pub location: ResourceLocation,
226 pub entries: Arc<[DirectoryEntry]>,
227 pub state: ListingState,
228}
229impl DirectorySnapshot {
230 pub fn location_of(&self, entry: &DirectoryEntry) -> ResourceLocation {
231 ResourceLocation {
232 filesystem: self.location.filesystem.clone(),
233 path: self.location.path.join(entry.name.as_path()),
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 #[test]
242 fn names_are_native_components_not_paths_or_urls() {
243 for rejected in ["", ".", "..", "/absolute", "parent/child", "child/", "a\0b"] {
244 assert!(EntryName::new(rejected.into()).is_err());
245 }
246 for accepted in ["ssh:literal", "-dash", "space name", "line\nbreak", "界"] {
247 let name = EntryName::new(accepted.into()).unwrap();
248 assert_eq!(name.as_path(), Path::new(accepted));
249 assert!(!name.display().contains('\n'));
250 }
251 }
252}