Skip to main content

resource_fork/
lib.rs

1//! Utilities for accessing Resource Forks
2//!
3//! Resource Forks are an alternate data stream for files on MacOS
4
5#![deny(unnameable_types, unreachable_pub, missing_docs)]
6
7use libc::XATTR_SHOWCOMPRESSION;
8use std::ffi::CStr;
9use std::fs::File;
10use std::io::{Read, Seek, SeekFrom};
11use std::os::unix::io::AsRawFd;
12use std::{cmp, io, ptr};
13
14/// The Extended Attribute (xattr) name which holds the Resource Fork
15pub const XATTR_NAME: &CStr = c"com.apple.ResourceFork";
16
17/// A Handle to a Resource Fork
18///
19/// A Resource Fork is a macos specific file attribute that contains arbitrary
20/// binary data.
21pub struct ResourceFork<'a> {
22    file: &'a File,
23    position: u32,
24}
25
26impl<'a> ResourceFork<'a> {
27    /// Create a new Resource Fork handle
28    ///
29    /// Note that if the file does not already have a resource fork, it will
30    /// only be created when the first write is performed.
31    #[must_use]
32    pub fn new(file: &'a File) -> Self {
33        Self { file, position: 0 }
34    }
35
36    /// Returns the current position of the resource fork
37    #[must_use]
38    pub fn position(&self) -> u32 {
39        self.position
40    }
41
42    /// Seek to a new position in the resource fork infallibly
43    pub fn set_position(&mut self, position: u32) {
44        self.position = position;
45    }
46
47    /// Remove the resource fork from the file
48    ///
49    /// This will remove any existing resource fork
50    ///
51    /// Note that this does not reset the current offset, it may be desired to
52    /// seek to the beginning of the resource fork after calling this, if you wish to
53    /// continue writing to the resource fork
54    pub fn delete(&mut self) -> io::Result<()> {
55        // SAFETY:
56        //   fd is valid because we have a handle to the file
57        //   xattr name is valid, and null terminated because it's a static CStr
58        let rc = unsafe {
59            libc::fremovexattr(
60                self.file.as_raw_fd(),
61                XATTR_NAME.as_ptr(),
62                XATTR_SHOWCOMPRESSION,
63            )
64        };
65        if rc != 0 {
66            return Err(io::Error::last_os_error());
67        }
68        Ok(())
69    }
70}
71
72impl io::Write for ResourceFork<'_> {
73    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
74        let len: u32 = buf
75            .len()
76            .try_into()
77            .map_err(|_| io::ErrorKind::InvalidInput)?;
78        let end_offset = self
79            .position
80            .checked_add(len)
81            .ok_or_else(|| io::Error::other("unable to fit resource fork in 32 bits"))?;
82        // SAFETY:
83        // fd is valid
84        // xattr name is valid
85        let rc = unsafe {
86            libc::fsetxattr(
87                self.file.as_raw_fd(),
88                XATTR_NAME.as_ptr(),
89                buf.as_ptr().cast(),
90                buf.len(),
91                self.position,
92                XATTR_SHOWCOMPRESSION,
93            )
94        };
95        if rc != 0 {
96            return Err(io::Error::last_os_error());
97        }
98        self.position = end_offset;
99        Ok(buf.len())
100    }
101
102    fn flush(&mut self) -> io::Result<()> {
103        Ok(())
104    }
105}
106
107impl Read for ResourceFork<'_> {
108    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
109        // Despite the manpage for getxattr saying:
110        // > On success, the size of the extended attribute data is returned
111        // it actually returns the size remaining _after_ the passed index
112
113        // SAFETY:
114        //   fd is valid because we have a handle to the file
115        //   xattr name is valid, and null terminated because it's a static CStr
116        //   buf is valid, and writable for up to len() bytes because it's passed as a mut slice
117        let rc = unsafe {
118            libc::fgetxattr(
119                self.file.as_raw_fd(),
120                XATTR_NAME.as_ptr(),
121                buf.as_mut_ptr().cast(),
122                buf.len(),
123                self.position,
124                XATTR_SHOWCOMPRESSION,
125            )
126        };
127        let remaining_len = if rc < 0 {
128            let e = io::Error::last_os_error();
129            if e.raw_os_error() == Some(libc::ENOATTR) {
130                0
131            } else {
132                return Err(e);
133            }
134        } else {
135            rc as usize
136        };
137        let bytes_read = cmp::min(remaining_len, buf.len());
138        self.position += u32::try_from(bytes_read).unwrap();
139        Ok(bytes_read)
140    }
141}
142
143impl Seek for ResourceFork<'_> {
144    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
145        let new_offset: u32 = match pos {
146            SeekFrom::Start(i) => i.try_into().map_err(|_| io::ErrorKind::InvalidInput)?,
147            SeekFrom::End(i) => {
148                // SAFETY:
149                // fd is valid because we have a handle to the file
150                // xattr name is valid, and null terminated because it's a static CStr
151                // value == NULL && size == 0 is allowed, to just return the length of the value
152                let mut rc = unsafe {
153                    libc::fgetxattr(
154                        self.file.as_raw_fd(),
155                        XATTR_NAME.as_ptr(),
156                        ptr::null_mut(),
157                        0,
158                        0,
159                        XATTR_SHOWCOMPRESSION,
160                    )
161                };
162                if rc < 0 {
163                    let e = io::Error::last_os_error();
164                    if e.raw_os_error() == Some(libc::ENOATTR) {
165                        rc = 0;
166                    } else {
167                        return Err(e);
168                    }
169                }
170                let end: u64 = rc.try_into().unwrap();
171                let offset = end
172                    .checked_add_signed(i)
173                    .ok_or(io::ErrorKind::InvalidInput)?;
174                offset.try_into().map_err(|_| io::ErrorKind::InvalidInput)?
175            }
176            SeekFrom::Current(i) => {
177                let current_offset = u64::from(self.position);
178                let offset = current_offset
179                    .checked_add_signed(i)
180                    .ok_or(io::ErrorKind::InvalidInput)?;
181                offset.try_into().map_err(|_| io::ErrorKind::InvalidInput)?
182            }
183        };
184        self.position = new_offset;
185        Ok(new_offset.into())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use std::ffi::CString;
193    use std::fs;
194    use std::io::Write;
195    use std::os::unix::ffi::OsStrExt;
196    use tempfile::NamedTempFile;
197
198    mod xattr {
199        use std::ffi::CStr;
200        use std::{io, ptr};
201
202        pub(super) fn len(path: &CStr, xattr_name: &CStr) -> io::Result<Option<usize>> {
203            // SAFETY:
204            // path/xattr_name are valid pointers and are null terminated
205            // value == NULL, size == 0 is allowed to just return the size
206            let rc = unsafe {
207                libc::getxattr(
208                    path.as_ptr(),
209                    xattr_name.as_ptr(),
210                    ptr::null_mut(),
211                    0,
212                    0,
213                    libc::XATTR_SHOWCOMPRESSION,
214                )
215            };
216            if rc == -1 {
217                let last_error = io::Error::last_os_error();
218                return if last_error.raw_os_error() == Some(libc::ENOATTR) {
219                    Ok(None)
220                } else {
221                    Err(last_error)
222                };
223            }
224            Ok(Some(rc as usize))
225        }
226
227        pub(super) fn is_present(path: &CStr, xattr_name: &CStr) -> io::Result<bool> {
228            len(path, xattr_name).map(|len| len.is_some())
229        }
230    }
231
232    #[test]
233    fn no_create_without_write() {
234        let file = NamedTempFile::new().unwrap();
235        let mut rfork = ResourceFork::new(file.as_file());
236        let path = CString::new(file.path().as_os_str().as_bytes()).unwrap();
237        assert!(!xattr::is_present(&path, XATTR_NAME).unwrap());
238        assert_eq!(rfork.seek(SeekFrom::Start(10)).unwrap(), 10);
239        assert!(!xattr::is_present(&path, XATTR_NAME).unwrap());
240        assert_eq!(rfork.seek(SeekFrom::Current(1)).unwrap(), 11);
241        assert!(!xattr::is_present(&path, XATTR_NAME).unwrap());
242        assert_eq!(rfork.seek(SeekFrom::End(0)).unwrap(), 0);
243        assert!(!xattr::is_present(&path, XATTR_NAME).unwrap());
244    }
245
246    #[test]
247    fn create_by_write() {
248        let file = NamedTempFile::new().unwrap();
249        let mut rfork = ResourceFork::new(file.as_file());
250        let path = CString::new(file.path().as_os_str().as_bytes()).unwrap();
251
252        let data = b"hi there";
253        assert_eq!(rfork.write(data).unwrap(), data.len());
254        rfork.flush().unwrap();
255        assert!(xattr::is_present(&path, XATTR_NAME).unwrap());
256        let content = fs::read(file.path().join("..namedfork/rsrc")).unwrap();
257        assert_eq!(content, data);
258    }
259
260    #[test]
261    fn read_not_exist() {
262        let file = tempfile::tempfile().unwrap();
263        let mut rfork = ResourceFork::new(&file);
264
265        let mut buf = [0; 1024];
266        let mut buf_vec = Vec::new();
267        assert_eq!(rfork.read(&mut buf).unwrap(), 0);
268        assert_eq!(rfork.read_to_end(&mut buf_vec).unwrap(), 0);
269        assert!(buf_vec.is_empty());
270
271        assert_eq!(rfork.seek(SeekFrom::Start(10)).unwrap(), 10);
272        assert_eq!(rfork.read(&mut buf).unwrap(), 0);
273        assert_eq!(rfork.read_to_end(&mut buf_vec).unwrap(), 0);
274        assert!(buf_vec.is_empty());
275    }
276
277    #[test]
278    fn read_past_end() {
279        let file = tempfile::tempfile().unwrap();
280        let mut rfork = ResourceFork::new(&file);
281
282        let data = b"hi there";
283        assert_eq!(rfork.write(data).unwrap(), data.len());
284
285        let mut buf = [0; 1024];
286        let mut buf_vec = vec![1, 2, 3];
287        // at end already, should empty read
288        assert_eq!(rfork.read(&mut buf).unwrap(), 0);
289        assert_eq!(rfork.position as usize, data.len());
290
291        assert_eq!(
292            rfork.seek(SeekFrom::Current(10)).unwrap(),
293            data.len() as u64 + 10
294        );
295        assert_eq!(rfork.read(&mut buf).unwrap(), 0);
296        assert_eq!(rfork.read_to_end(&mut buf_vec).unwrap(), 0);
297        assert_eq!(buf_vec, [1, 2, 3]);
298    }
299
300    #[test]
301    fn read() {
302        let file = tempfile::tempfile().unwrap();
303        let mut rfork = ResourceFork::new(&file);
304
305        let data = b"hi there";
306        assert_eq!(rfork.write(data).unwrap(), data.len());
307
308        assert_eq!(
309            rfork.seek(SeekFrom::Current(-1)).unwrap(),
310            data.len() as u64 - 1
311        );
312
313        let mut buf = [0; 1024];
314        let mut buf_vec = vec![1, 2, 3];
315        assert_eq!(rfork.read_to_end(&mut buf_vec).unwrap(), 1);
316        assert_eq!(buf_vec, [1, 2, 3, b'e']);
317
318        rfork.rewind().unwrap();
319        assert_eq!(rfork.read(&mut buf).unwrap(), data.len());
320        assert_eq!(&buf[..data.len()], data);
321        // We read it all
322        assert_eq!(rfork.read(&mut buf).unwrap(), 0);
323    }
324}