1#![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
14pub const XATTR_NAME: &CStr = c"com.apple.ResourceFork";
16
17pub struct ResourceFork<'a> {
22 file: &'a File,
23 position: u32,
24}
25
26impl<'a> ResourceFork<'a> {
27 #[must_use]
32 pub fn new(file: &'a File) -> Self {
33 Self { file, position: 0 }
34 }
35
36 #[must_use]
38 pub fn position(&self) -> u32 {
39 self.position
40 }
41
42 pub fn set_position(&mut self, position: u32) {
44 self.position = position;
45 }
46
47 pub fn delete(&mut self) -> io::Result<()> {
55 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 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 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 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 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 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 assert_eq!(rfork.read(&mut buf).unwrap(), 0);
323 }
324}