|
1 | 1 | //! File related types |
2 | 2 |
|
3 | | -use bitflags::bitflags; |
4 | | -use neotron_ffi::FfiString; |
5 | | - |
6 | | -/// Represents a (borrowed) path to file. |
7 | | -/// |
8 | | -/// Neotron OS uses the following format for file paths: |
9 | | -/// |
10 | | -/// `<disk>:/[<directory>/]+<filename>.<extension>` |
11 | | -/// |
12 | | -/// Unlike on MS-DOS, the `disk` specifier portion is not limited to a single |
13 | | -/// ASCII letter and can be any UTF-8 string that does not contain `:` or `/`. |
14 | | -/// |
15 | | -/// Paths are a sub-set of UTF-8 strings in this API, but be aware that not all |
16 | | -/// filesystems support all Unicode characters. In particular FAT16 and FAT32 |
17 | | -/// volumes are likely to be limited to only `A-Z`, `a-z`, `0-9` and |
18 | | -/// `$%-_@~\`!(){}^#&`. This API will expressly disallow UTF-8 codepoints below |
19 | | -/// 32 (i.e. C0 control characters) to avoid confusion. |
20 | | -/// |
21 | | -/// Paths are case-preserving but may not be case-sensitive. Paths may contain |
22 | | -/// spaces, if your filesystem supports it. |
23 | | -/// |
24 | | -/// Here are some examples of valid paths: |
25 | | -/// |
26 | | -/// ```text |
27 | | -/// Documents/2023/June/Sales in €.xls - relative to the Current Directory |
28 | | -/// HD0:/MYDOCU~1/SALES.TXT - a file on drive HD0 |
29 | | -/// SD0:/MYDOCU~1/ - a directory on drive SD0 |
30 | | -/// SD0:/BOOTLDR - a file on drive SD0, with no file extension |
31 | | -/// CON$: - a special device file |
32 | | -/// SER0$:/bps=9600/parity=N/timeout=100: - a special device file with parameters |
33 | | -/// ``` |
34 | | -#[repr(C)] |
35 | | -pub struct Path<'a>(FfiString<'a>); |
36 | | - |
37 | | -impl<'a> Path<'a> { |
38 | | - /// The character that separates one directory name from another directory name. |
39 | | - pub const PATH_SEP: char = '/'; |
40 | | - |
41 | | - /// The character that separates drive specifiers from directories. |
42 | | - pub const DRIVE_SEP: char = ':'; |
43 | | - |
44 | | - /// Create a path from a string. |
45 | | - /// |
46 | | - /// If the given string is not a valid path, an `Err` is returned. |
47 | | - pub fn new(path_str: &'a str) -> Result<Path<'a>, crate::Error> { |
48 | | - // No empty paths in drive specifier |
49 | | - if path_str.is_empty() { |
50 | | - return Err(crate::Error::InvalidPath); |
51 | | - } |
52 | | - |
53 | | - if let Some((drive_specifier, directory_path)) = path_str.split_once(Self::DRIVE_SEP) { |
54 | | - if drive_specifier.contains(Self::PATH_SEP) { |
55 | | - // No slashes in drive specifier |
56 | | - return Err(crate::Error::InvalidPath); |
57 | | - } |
58 | | - if directory_path.contains(Self::DRIVE_SEP) { |
59 | | - // No colons in directory path |
60 | | - return Err(crate::Error::InvalidPath); |
61 | | - } |
62 | | - if !directory_path.is_empty() && !directory_path.starts_with(Self::PATH_SEP) { |
63 | | - // No relative paths if drive is specified. An empty path is OK (it means "/") |
64 | | - return Err(crate::Error::InvalidPath); |
65 | | - } |
66 | | - } else if path_str.starts_with(Self::PATH_SEP) { |
67 | | - // No absolute paths if drive is not specified |
68 | | - return Err(crate::Error::InvalidPath); |
69 | | - } |
70 | | - for ch in path_str.chars() { |
71 | | - if ch.is_control() { |
72 | | - // No control characters allowed |
73 | | - return Err(crate::Error::InvalidPath); |
74 | | - } |
75 | | - } |
76 | | - Ok(Path(FfiString::new(path_str))) |
77 | | - } |
78 | | - |
79 | | - /// Is this an absolute path? |
80 | | - /// |
81 | | - /// Absolute paths have drive specifiers. Relative paths do not. |
82 | | - pub fn is_absolute_path(&self) -> bool { |
83 | | - self.drive_specifier().is_some() |
84 | | - } |
| 3 | +// ============================================================================ |
| 4 | +// Imports |
| 5 | +// ============================================================================ |
85 | 6 |
|
86 | | - /// Get the drive specifier for this path. |
87 | | - /// |
88 | | - /// * A path like `DS0:/FOO/BAR.TXT` has a drive specifier of `DS0`. |
89 | | - /// * A path like `BAR.TXT` has no drive specifier. |
90 | | - pub fn drive_specifier(&self) -> Option<&str> { |
91 | | - let path_str = self.0.as_str(); |
92 | | - if let Some((drive_specifier, _directory_path)) = path_str.split_once(Self::DRIVE_SEP) { |
93 | | - Some(drive_specifier) |
94 | | - } else { |
95 | | - None |
96 | | - } |
97 | | - } |
98 | | - |
99 | | - /// Get the drive path portion. |
100 | | - /// |
101 | | - /// That is, everything after the directory specifier. |
102 | | - pub fn drive_path(&self) -> Option<&str> { |
103 | | - let path_str = self.0.as_str(); |
104 | | - if let Some((_drive_specifier, drive_path)) = path_str.split_once(Self::DRIVE_SEP) { |
105 | | - if drive_path.is_empty() { |
106 | | - Some("/") |
107 | | - } else { |
108 | | - Some(drive_path) |
109 | | - } |
110 | | - } else { |
111 | | - Some(path_str) |
112 | | - } |
113 | | - } |
| 7 | +use bitflags::bitflags; |
114 | 8 |
|
115 | | - /// Get the directory portion of this path. |
116 | | - /// |
117 | | - /// * A path like `DS0:/FOO/BAR.TXT` has a directory portion of `/FOO`. |
118 | | - /// * A path like `DS0:/FOO/BAR/` has a directory portion of `/FOO/BAR`. |
119 | | - /// * A path like `BAR.TXT` has no directory portion. |
120 | | - pub fn directory(&self) -> Option<&str> { |
121 | | - let Some(drive_path) = self.drive_path() else { |
122 | | - return None; |
123 | | - }; |
124 | | - if let Some((directory, _filename)) = drive_path.rsplit_once(Self::PATH_SEP) { |
125 | | - if directory.is_empty() { |
126 | | - None |
127 | | - } else { |
128 | | - Some(directory) |
129 | | - } |
130 | | - } else { |
131 | | - Some(drive_path) |
132 | | - } |
133 | | - } |
| 9 | +// ============================================================================ |
| 10 | +// Constants |
| 11 | +// ============================================================================ |
134 | 12 |
|
135 | | - /// Get the filename portion of this path. This filename will include the file extension, if any. |
136 | | - /// |
137 | | - /// * A path like `DS0:/FOO/BAR.TXT` has a filename portion of `/BAR.TXT`. |
138 | | - /// * A path like `DS0:/FOO` has a filename portion of `/FOO`. |
139 | | - /// * A path like `DS0:/FOO/` has no filename portion (so it's important directories have a trailing `/`) |
140 | | - pub fn filename(&self) -> Option<&str> { |
141 | | - let Some(drive_path) = self.drive_path() else { |
142 | | - return None; |
143 | | - }; |
144 | | - if let Some((_directory, filename)) = drive_path.rsplit_once(Self::PATH_SEP) { |
145 | | - if filename.is_empty() { |
146 | | - None |
147 | | - } else { |
148 | | - Some(filename) |
149 | | - } |
150 | | - } else { |
151 | | - Some(drive_path) |
152 | | - } |
153 | | - } |
| 13 | +// None |
154 | 14 |
|
155 | | - /// Get the filename extension portion of this path. |
156 | | - /// |
157 | | - /// A path like `DS0:/FOO/BAR.TXT` has a filename extension portion of `TXT`. |
158 | | - /// A path like `DS0:/FOO/BAR` has no filename extension portion. |
159 | | - pub fn extension(&self) -> Option<&str> { |
160 | | - let Some(filename) = self.filename() else { |
161 | | - return None; |
162 | | - }; |
163 | | - if let Some((_basename, extension)) = filename.rsplit_once('.') { |
164 | | - Some(extension) |
165 | | - } else { |
166 | | - None |
167 | | - } |
168 | | - } |
169 | | -} |
| 15 | +// ============================================================================ |
| 16 | +// Types |
| 17 | +// ============================================================================ |
170 | 18 |
|
171 | 19 | /// Represents an open file |
172 | 20 | #[repr(C)] |
@@ -285,55 +133,18 @@ pub struct Time { |
285 | 133 | pub seconds: u8, |
286 | 134 | } |
287 | 135 |
|
288 | | -#[cfg(test)] |
289 | | -mod tests { |
290 | | - use super::*; |
| 136 | +// ============================================================================ |
| 137 | +// Functions |
| 138 | +// ============================================================================ |
291 | 139 |
|
292 | | - #[test] |
293 | | - fn full_path() { |
294 | | - let path_str = "HD0:/DOCUMENTS/JUNE/SALES.TXT"; |
295 | | - let path = Path::new(path_str).unwrap(); |
296 | | - assert!(path.is_absolute_path()); |
297 | | - assert_eq!(path.drive_specifier(), Some("HD0")); |
298 | | - assert_eq!(path.drive_path(), Some("/DOCUMENTS/JUNE/SALES.TXT")); |
299 | | - assert_eq!(path.directory(), Some("/DOCUMENTS/JUNE")); |
300 | | - assert_eq!(path.filename(), Some("SALES.TXT")); |
301 | | - assert_eq!(path.extension(), Some("TXT")); |
302 | | - } |
| 140 | +// None |
303 | 141 |
|
304 | | - #[test] |
305 | | - fn relative_path() { |
306 | | - let path_str = "DOCUMENTS/JUNE/SALES.TXT"; |
307 | | - let path = Path::new(path_str).unwrap(); |
308 | | - assert!(!path.is_absolute_path()); |
309 | | - assert_eq!(path.drive_specifier(), None); |
310 | | - assert_eq!(path.drive_path(), Some("DOCUMENTS/JUNE/SALES.TXT")); |
311 | | - assert_eq!(path.directory(), Some("DOCUMENTS/JUNE")); |
312 | | - assert_eq!(path.filename(), Some("SALES.TXT")); |
313 | | - assert_eq!(path.extension(), Some("TXT")); |
314 | | - } |
| 142 | +// ============================================================================ |
| 143 | +// Tests |
| 144 | +// ============================================================================ |
315 | 145 |
|
316 | | - #[test] |
317 | | - fn full_dir() { |
318 | | - let path_str = "HD0:/DOCUMENTS/JUNE/"; |
319 | | - let path = Path::new(path_str).unwrap(); |
320 | | - assert!(path.is_absolute_path()); |
321 | | - assert_eq!(path.drive_specifier(), Some("HD0")); |
322 | | - assert_eq!(path.drive_path(), Some("/DOCUMENTS/JUNE/")); |
323 | | - assert_eq!(path.directory(), Some("/DOCUMENTS/JUNE")); |
324 | | - assert_eq!(path.filename(), None); |
325 | | - assert_eq!(path.extension(), None); |
326 | | - } |
| 146 | +// None |
327 | 147 |
|
328 | | - #[test] |
329 | | - fn relative_dir() { |
330 | | - let path_str = "DOCUMENTS/"; |
331 | | - let path = Path::new(path_str).unwrap(); |
332 | | - assert!(!path.is_absolute_path()); |
333 | | - assert_eq!(path.drive_specifier(), None); |
334 | | - assert_eq!(path.drive_path(), Some("DOCUMENTS/")); |
335 | | - assert_eq!(path.directory(), Some("DOCUMENTS")); |
336 | | - assert_eq!(path.filename(), None); |
337 | | - assert_eq!(path.extension(), None); |
338 | | - } |
339 | | -} |
| 148 | +// ============================================================================ |
| 149 | +// End of File |
| 150 | +// ============================================================================ |
0 commit comments