├── .gitignore ├── Cargo.toml ├── LICENSE ├── finder_info_bin ├── Cargo.toml └── src │ └── bin │ └── finderinfo.rs ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finder_info" 3 | version = "0.2.3" 4 | authors = ["Dropbox Engineering "] 5 | license = "Apache-2.0" 6 | description = "A library to parse Apple HFS/HFS+/APFS FinderInfo attribute." 7 | repository = "https://github.com/dropbox/finderinfo-rust" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | byteorder = "1.1" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /finder_info_bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finder_info_bin" 3 | version = "0.2.0" 4 | authors = ["Dropbox Engineering "] 5 | license = "Apache-2.0" 6 | description = "A utility to parse the Apple HFS/HFS+/APFS FinderInfo attribute." 7 | repository = "https://github.com/dropbox/finderinfo-rust" 8 | edition = "2018" 9 | 10 | [[bin]] 11 | name = "finderinfo" 12 | 13 | [features] 14 | default = [] 15 | # this feature enables the ability to read and write the FinderInfo xattr on MacOS. 16 | xattr = ["libc"] 17 | 18 | [dependencies] 19 | finder_info = { path = "..", version = "0.2" } 20 | 21 | cfg-if = "0.1" 22 | docopt = "1.0" 23 | hex = "0.3" 24 | serde = "1.0" 25 | serde_derive = "1.0" 26 | 27 | libc = { version = "0.2", optional = true } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # finderinfo 2 | 3 | A library to parse Apple HFS/HFS+/APFS FinderInfo attribute. 4 | 5 | On modern MacOS systems, objects in the filesystem can have an extended attribute called `com.apple.FinderInfo`. This 6 | attribute is 32 bytes long and largely undocumented. It turns out that this attribute is actually the old HFS Finder 7 | Info struct in the first 16 bytes, and the Extended Finder Info struct in the second 16 bytes. This library provides a 8 | mechanism by which a Rust program can programmatically interact with these structures. 9 | 10 | This crate also provides an executable `finderinfo`, which is a small utility that can parse and display the contents of 11 | the Finder Info blob. If built with the `xattr` feature, the library is able to read and write the 12 | `com.apple.FinderInfo` extended attribute on MacOS systems. 13 | 14 | ## Example 15 | 16 | ```rust 17 | let buf = vec![ 18 | 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 19 | 0x40u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 20 | 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 21 | 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 22 | ]; 23 | let finder_info = FinderInfoFolder::read(&mut io::Cursor::new(buf)); 24 | println!("{:?}", finder_info); 25 | ``` 26 | -------------------------------------------------------------------------------- /finder_info_bin/src/bin/finderinfo.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | 4 | use cfg_if::cfg_if; 5 | use docopt::Docopt; 6 | use hex::FromHex; 7 | use serde_derive::Deserialize; 8 | 9 | use finder_info::{FinderInfoFile, FinderInfoFolder, OSType}; 10 | 11 | const USAGE: &'static str = " 12 | FinderInfo utility. 13 | 14 | Usage: 15 | finderinfo read 16 | finderinfo parse-hex (-d | -f) 17 | finderinfo read-filetype 18 | finderinfo write-filetype 19 | finderinfo (-h | --help) 20 | 21 | Options: 22 | -h --help Show this screen. 23 | -d Read FinderInfo as directory 24 | -f Read FinderInfo as file 25 | "; 26 | 27 | #[derive(Debug, Deserialize)] 28 | struct Args { 29 | arg_path: String, 30 | arg_value: String, 31 | arg_hex_data: String, 32 | cmd_read: bool, 33 | cmd_read_filetype: bool, 34 | cmd_write_filetype: bool, 35 | cmd_parse_hex: bool, 36 | flag_d: bool, 37 | } 38 | 39 | #[derive(Clone, Debug)] 40 | enum FinderInfo { 41 | File(FinderInfoFile), 42 | Directory(FinderInfoFolder), 43 | } 44 | 45 | cfg_if! { 46 | if #[cfg(all(feature = "xattr", target_os = "macos"))] { 47 | use std::ffi::CString; 48 | const FINDERINFO_XATTR_NAME: &'static str = "com.apple.FinderInfo"; 49 | 50 | fn read_finderinfo_from_path(path: &str) -> io::Result { 51 | let path_cstring = CString::new(path).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 52 | let xattr_name = 53 | CString::new(FINDERINFO_XATTR_NAME).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 54 | let mut buf = [0u8; 32]; 55 | 56 | let ret = unsafe { 57 | libc::getxattr( 58 | path_cstring.as_ptr(), 59 | xattr_name.as_ptr(), 60 | buf.as_mut_ptr() as *mut libc::c_void, 61 | buf.len(), 62 | 0, /* position */ 63 | 0, /* flags */ 64 | ) 65 | }; 66 | if ret == -1 { 67 | return Err(io::Error::last_os_error()); 68 | } else if ret != 32 { 69 | return Err(io::Error::new( 70 | io::ErrorKind::Interrupted, 71 | format!("only received {:?} bytes", ret), 72 | )); 73 | } 74 | 75 | let is_dir = fs::metadata(path)?.is_dir(); 76 | 77 | Ok(if is_dir { 78 | FinderInfo::Directory(FinderInfoFolder::read(&mut io::Cursor::new(buf))?) 79 | } else { 80 | FinderInfo::File(FinderInfoFile::read(&mut io::Cursor::new(buf))?) 81 | }) 82 | } 83 | 84 | fn write_finderinfo_to_path(path: &str, fi: FinderInfo) -> io::Result<()> { 85 | let mut cursor = io::Cursor::new(vec![]); 86 | match fi { 87 | FinderInfo::File(fi) => fi.write(&mut cursor)?, 88 | FinderInfo::Directory(fi) => fi.write(&mut cursor)?, 89 | } 90 | let bytes = cursor.into_inner(); 91 | let path_cstring = CString::new(path).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 92 | let xattr_name = 93 | CString::new(FINDERINFO_XATTR_NAME).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 94 | let ret = unsafe { 95 | libc::setxattr( 96 | path_cstring.as_ptr(), 97 | xattr_name.as_ptr(), 98 | bytes.as_ptr() as *const libc::c_void, 99 | bytes.len(), 100 | 0, /* position */ 101 | 0, /* flags */ 102 | ) 103 | }; 104 | if ret == -1 { 105 | return Err(io::Error::last_os_error()); 106 | } 107 | Ok(()) 108 | } 109 | } else { 110 | fn read_finderinfo_from_path(_path: &str) -> io::Result { 111 | Err(io::Error::new( 112 | io::ErrorKind::Other, 113 | "xattr i/o not supported", 114 | )) 115 | } 116 | 117 | fn write_finderinfo_to_path(_path: &str, _fi: FinderInfo) -> io::Result<()> { 118 | Err(io::Error::new( 119 | io::ErrorKind::Other, 120 | "xattr i/o not supported", 121 | )) 122 | } 123 | } 124 | } 125 | 126 | fn main() { 127 | let args: Args = Docopt::new(USAGE) 128 | .and_then(|d| d.deserialize()) 129 | .unwrap_or_else(|e| e.exit()); 130 | if args.cmd_parse_hex { 131 | let buf = Vec::from_hex(&args.arg_hex_data).expect("invalid hexadecimal string"); 132 | let finder_info = if args.flag_d { 133 | FinderInfo::Directory( 134 | FinderInfoFolder::read(&mut io::Cursor::new(buf)).expect("Read failed"), 135 | ) 136 | } else { 137 | FinderInfo::File(FinderInfoFile::read(&mut io::Cursor::new(buf)).expect("Read failed")) 138 | }; 139 | println!("{:#?}", finder_info); 140 | } 141 | if args.cmd_read || args.cmd_read_filetype { 142 | println!("Attempting to read FinderInfo from {:?}", args.arg_path); 143 | let finder_info = read_finderinfo_from_path(&args.arg_path); 144 | if args.cmd_read { 145 | println!("{:#?}", finder_info); 146 | } 147 | if args.cmd_read_filetype { 148 | match finder_info { 149 | Ok(FinderInfo::File(fi)) => println!("file type: {:?}", fi.file_info.fileType), 150 | _ => panic!("Not found"), 151 | } 152 | } 153 | } 154 | if args.cmd_write_filetype { 155 | println!("Attempting to read FinderInfo from {:?}", args.arg_path); 156 | let finder_info = read_finderinfo_from_path(&args.arg_path).unwrap_or_else(|_| { 157 | if fs::metadata(&args.arg_path).unwrap().is_dir() { 158 | panic!("attempted to set filetype on a directory") 159 | } 160 | FinderInfo::File(FinderInfoFile::default()) 161 | }); 162 | match finder_info { 163 | FinderInfo::File(mut fi) => { 164 | println!("Original filetype: {:?}", fi.file_info.fileType); 165 | let bytes = args.arg_value.into_bytes(); 166 | if bytes.len() != 4 { 167 | panic!("file type {:?} must be 4 bytes", bytes); 168 | } 169 | let new_filetype = OSType([bytes[0], bytes[1], bytes[2], bytes[3]]); 170 | println!("New filetype: {:?}", new_filetype); 171 | fi.file_info.fileType = new_filetype; 172 | 173 | write_finderinfo_to_path(&args.arg_path, FinderInfo::File(fi)).unwrap(); 174 | println!("Successfully wrote FinderInfo!"); 175 | } 176 | FinderInfo::Directory(fi) => panic!("target is not a file! {:?}", fi), 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_upper_case_globals)] 2 | 3 | //! Structs and functions to manipulate MacOS/HFS+ structs, e.g. com.apple.FinderInfo. 4 | //! 5 | //! Note that HFS+ is big-endian, and so all serialization/deserialization has to be byteswapped 6 | //! appropriately. APFS isn't big-endian, but it pretends pretty hard internally (and does so 7 | //! here). 8 | 9 | use std::fmt; 10 | use std::io::{self, Read, Write}; 11 | 12 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 13 | 14 | #[derive(Clone, Copy, Default, Eq, PartialEq)] 15 | pub struct OSType(pub [u8; 4]); 16 | 17 | impl fmt::Debug for OSType { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | let s = String::from_utf8(self.0.to_vec()); 20 | write!(f, "{:?}", s) 21 | } 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub mod constants { 26 | use super::OSType; 27 | 28 | // Finder flag constants 29 | /// Unused and reserved in System 7; set to 0. 30 | pub const kIsOnDesk: u16 = 0x0001; 31 | /// Three bits of color coding. 32 | pub const kColor: u16 = 0x000e; 33 | /// The file is an application that can be executed by multiple users simultaneously. Defined 34 | /// only for applications; otherwise, set to 0. 35 | pub const kIsShared: u16 = 0x0040; 36 | /// The file contains no 'INIT' resources; set to 0. Reserved for directories; set to 0. 37 | pub const kHasNoINITs: u16 = 0x0080; 38 | /// The Finder has recorded information from the file's bundle resource into the desktop 39 | /// database and given the file or folder a position on the desktop. 40 | pub const kHasBeenInited: u16 = 0x0100; 41 | /// The file or directory contains a customized icon. 42 | pub const kHasCustomIcon: u16 = 0x0400; 43 | /// For a file, this bit indicates that the file is a stationery pad. For directories, this bit 44 | /// is reserved--in which case, set to 0. 45 | pub const kIsStationery: u16 = 0x0800; 46 | /// The file or directory can't be renamed from the Finder, and the icon cannot be changed. 47 | pub const kNameLocked: u16 = 0x1000; 48 | /// For a file, this bit indicates that the file contains a bundle resource. For a directory, 49 | /// this bit indicates that the directory is a file package. Note that not all file packages 50 | /// have this bit set; many file packages are identified by other means, such as a recognized 51 | /// package extension in the name. The proper way to determine if an item is a package is 52 | /// through Launch Services. 53 | pub const kHasBundle: u16 = 0x2000; 54 | /// The file or directory is invisible from the Finder and from the Navigation Services dialogs. 55 | pub const kIsInvisible: u16 = 0x4000; 56 | /// For a file, this bit indicates that the file is an alias file. For directories, this bit is 57 | /// reserved--in which case, set to 0. 58 | pub const kIsAlias: u16 = 0x8000; 59 | /// Undocumented in Finder.h, but used by Finder to hide the extension of a file or folder. 60 | /// Name is not official. 61 | pub const kHideExtension: u16 = 0x0010; 62 | 63 | // Extended finder flag constants 64 | /// If set the other extended flags are ignored. 65 | pub const kExtendedFlagsAreInvalid: u16 = 0x8000; 66 | /// Set if the file or folder has a badge resource. 67 | pub const kExtendedFlagHasCustomBadge: u16 = 0x0100; 68 | /// Set if the file contains routing info resource. 69 | pub const kExtendedFlagHasRoutingInfo: u16 = 0x0004; 70 | 71 | // File type constants 72 | /// File type for a symlink. 73 | pub const kSymLinkFileType: OSType = OSType([0x73, 0x6c, 0x6e, 0x6b]); /* 'slnk' */ 74 | /// File type for the creator of a symlink. 75 | pub const kSymLinkCreator: OSType = OSType([0x72, 0x68, 0x61, 0x70]); /* 'rhap' */ 76 | } 77 | 78 | #[derive(Clone, Debug, Default)] 79 | #[repr(C)] 80 | pub struct Point { 81 | pub v: i16, 82 | pub h: i16, 83 | } 84 | 85 | impl Point { 86 | pub fn read(r: &mut R) -> io::Result { 87 | let v = r.read_i16::()?; 88 | let h = r.read_i16::()?; 89 | Ok(Point { v, h }) 90 | } 91 | 92 | pub fn write(&self, w: &mut W) -> io::Result<()> { 93 | w.write_i16::(self.v)?; 94 | w.write_i16::(self.h)?; 95 | Ok(()) 96 | } 97 | } 98 | 99 | #[derive(Clone, Debug, Default)] 100 | #[repr(C)] 101 | pub struct Rect { 102 | pub top: i16, 103 | pub left: i16, 104 | pub bottom: i16, 105 | pub right: i16, 106 | } 107 | 108 | impl Rect { 109 | pub fn read(r: &mut R) -> io::Result { 110 | let top = r.read_i16::()?; 111 | let left = r.read_i16::()?; 112 | let bottom = r.read_i16::()?; 113 | let right = r.read_i16::()?; 114 | Ok(Rect { 115 | top, 116 | left, 117 | bottom, 118 | right, 119 | }) 120 | } 121 | 122 | pub fn write(&self, w: &mut W) -> io::Result<()> { 123 | w.write_i16::(self.top)?; 124 | w.write_i16::(self.left)?; 125 | w.write_i16::(self.bottom)?; 126 | w.write_i16::(self.right)?; 127 | Ok(()) 128 | } 129 | } 130 | 131 | #[derive(Clone, Copy, Default, Eq, PartialEq)] 132 | pub struct FinderFlags(u16); 133 | 134 | impl FinderFlags { 135 | pub fn color(&self) -> Option { 136 | LabelColor::from_u8((self.0 & constants::kColor) as u8) 137 | } 138 | 139 | pub fn set_color(&mut self, color: Option) { 140 | self.0 &= !constants::kColor; 141 | self.0 |= u16::from(LabelColor::to_u8(color)); 142 | } 143 | 144 | pub fn is_shared(&self) -> bool { 145 | self.0 & constants::kIsShared != 0 146 | } 147 | 148 | pub fn set_is_shared(&mut self, value: bool) { 149 | if value { 150 | self.0 |= constants::kIsShared; 151 | } else { 152 | self.0 &= !constants::kIsShared; 153 | } 154 | } 155 | 156 | pub fn has_no_inits(&self) -> bool { 157 | self.0 & constants::kHasNoINITs != 0 158 | } 159 | 160 | pub fn set_has_no_inits(&mut self, value: bool) { 161 | if value { 162 | self.0 |= constants::kHasNoINITs; 163 | } else { 164 | self.0 &= !constants::kHasNoINITs; 165 | } 166 | } 167 | 168 | pub fn has_been_inited(&self) -> bool { 169 | self.0 & constants::kHasBeenInited != 0 170 | } 171 | 172 | pub fn set_has_been_inited(&mut self, value: bool) { 173 | if value { 174 | self.0 |= constants::kHasBeenInited; 175 | } else { 176 | self.0 &= !constants::kHasBeenInited; 177 | } 178 | } 179 | 180 | pub fn has_custom_icon(&self) -> bool { 181 | self.0 & constants::kHasCustomIcon != 0 182 | } 183 | 184 | pub fn set_has_custom_icon(&mut self, value: bool) { 185 | if value { 186 | self.0 |= constants::kHasCustomIcon; 187 | } else { 188 | self.0 &= !constants::kHasCustomIcon; 189 | } 190 | } 191 | 192 | pub fn has_hidden_extension(&self) -> bool { 193 | self.0 & constants::kHideExtension != 0 194 | } 195 | 196 | pub fn set_has_hidden_extension(&mut self, value: bool) { 197 | if value { 198 | self.0 |= constants::kHideExtension; 199 | } else { 200 | self.0 &= !constants::kHideExtension; 201 | } 202 | } 203 | 204 | pub fn is_stationery(&self) -> bool { 205 | self.0 & constants::kIsStationery != 0 206 | } 207 | 208 | pub fn set_is_stationery(&mut self, value: bool) { 209 | if value { 210 | self.0 |= constants::kIsStationery; 211 | } else { 212 | self.0 &= !constants::kIsStationery; 213 | } 214 | } 215 | 216 | pub fn name_locked(&self) -> bool { 217 | self.0 & constants::kNameLocked != 0 218 | } 219 | 220 | pub fn set_name_locked(&mut self, value: bool) { 221 | if value { 222 | self.0 |= constants::kNameLocked; 223 | } else { 224 | self.0 &= !constants::kNameLocked; 225 | } 226 | } 227 | 228 | pub fn has_bundle(&self) -> bool { 229 | self.0 & constants::kHasBundle != 0 230 | } 231 | 232 | pub fn set_has_bundle(&mut self, value: bool) { 233 | if value { 234 | self.0 |= constants::kHasBundle; 235 | } else { 236 | self.0 &= !constants::kHasBundle; 237 | } 238 | } 239 | 240 | pub fn is_invisible(&self) -> bool { 241 | self.0 & constants::kIsInvisible != 0 242 | } 243 | 244 | pub fn set_is_invisible(&mut self, value: bool) { 245 | if value { 246 | self.0 |= constants::kIsInvisible; 247 | } else { 248 | self.0 &= !constants::kIsInvisible; 249 | } 250 | } 251 | 252 | pub fn is_alias(&self) -> bool { 253 | self.0 & constants::kIsAlias != 0 254 | } 255 | 256 | pub fn set_is_alias(&mut self, value: bool) { 257 | if value { 258 | self.0 |= constants::kIsAlias; 259 | } else { 260 | self.0 &= !constants::kIsAlias; 261 | } 262 | } 263 | } 264 | 265 | impl fmt::Debug for FinderFlags { 266 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 267 | let mut flags = vec![]; 268 | if let Some(color) = self.color() { 269 | flags.push(format!("{:?}", color)); 270 | } 271 | if self.is_shared() { 272 | flags.push("kIsShared".to_string()); 273 | } 274 | if self.has_no_inits() { 275 | flags.push("kHasNoINITs".to_string()); 276 | } 277 | if self.has_been_inited() { 278 | flags.push("kHasBeenInited".to_string()); 279 | } 280 | if self.has_custom_icon() { 281 | flags.push("kHasCustomIcon".to_string()); 282 | } 283 | if self.is_stationery() { 284 | flags.push("kIsStationery".to_string()); 285 | } 286 | if self.name_locked() { 287 | flags.push("kNameLocked".to_string()); 288 | } 289 | if self.has_bundle() { 290 | flags.push("kHasBundle".to_string()); 291 | } 292 | if self.is_invisible() { 293 | flags.push("kIsInvisible".to_string()); 294 | } 295 | if self.is_alias() { 296 | flags.push("kIsAlias".to_string()); 297 | } 298 | if self.has_hidden_extension() { 299 | flags.push("kHideExtension".to_string()); 300 | } 301 | f.debug_struct("FinderFlags") 302 | .field("raw", &self.0) 303 | .field("flags", &flags) 304 | .finish() 305 | } 306 | } 307 | 308 | impl From for FinderFlags { 309 | fn from(s: u16) -> FinderFlags { 310 | FinderFlags(s) 311 | } 312 | } 313 | impl From for u16 { 314 | fn from(f: FinderFlags) -> u16 { 315 | f.0 316 | } 317 | } 318 | 319 | // TODO(robert): In MacOS 10.10 and above, the `LabelColor` is no longer stored in the 320 | // `com.apple.FinderInfo` attribute but is instead stored in a `bplist` format. The last tag-string 321 | // in the `bplist` which corresponds to a color is the one which we should set in the 322 | // `com.apple.FinderInfo` attribute. We should synchronize these on write/read to be 323 | // cross-compatible with MacOS 10.9. 324 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 325 | pub enum LabelColor { 326 | Gray, 327 | Green, 328 | Purple, 329 | Blue, 330 | Yellow, 331 | Red, 332 | Orange, 333 | } 334 | 335 | impl LabelColor { 336 | pub fn from_u8(b: u8) -> Option { 337 | match b { 338 | 0x02 => Some(LabelColor::Gray), 339 | 0x04 => Some(LabelColor::Green), 340 | 0x06 => Some(LabelColor::Purple), 341 | 0x08 => Some(LabelColor::Blue), 342 | 0x0a => Some(LabelColor::Yellow), 343 | 0x0c => Some(LabelColor::Red), 344 | 0x0e => Some(LabelColor::Orange), 345 | _ => None, 346 | } 347 | } 348 | 349 | pub fn to_u8(c: Option) -> u8 { 350 | match c { 351 | None => 0x00, 352 | Some(LabelColor::Gray) => 0x02, 353 | Some(LabelColor::Green) => 0x04, 354 | Some(LabelColor::Purple) => 0x06, 355 | Some(LabelColor::Blue) => 0x08, 356 | Some(LabelColor::Yellow) => 0x0a, 357 | Some(LabelColor::Red) => 0x0c, 358 | Some(LabelColor::Orange) => 0x0e, 359 | } 360 | } 361 | 362 | pub fn to_str(c: LabelColor) -> &'static str { 363 | match c { 364 | LabelColor::Gray => "Gray", 365 | LabelColor::Green => "Green", 366 | LabelColor::Purple => "Purple", 367 | LabelColor::Blue => "Blue", 368 | LabelColor::Yellow => "Yellow", 369 | LabelColor::Red => "Red", 370 | LabelColor::Orange => "Orange", 371 | } 372 | } 373 | 374 | pub fn from_str(s: &str) -> Option { 375 | match s { 376 | "Gray" => Some(LabelColor::Gray), 377 | "Green" => Some(LabelColor::Green), 378 | "Purple" => Some(LabelColor::Purple), 379 | "Blue" => Some(LabelColor::Blue), 380 | "Yellow" => Some(LabelColor::Yellow), 381 | "Red" => Some(LabelColor::Red), 382 | "Orange" => Some(LabelColor::Orange), 383 | _ => None, 384 | } 385 | } 386 | } 387 | 388 | #[derive(Clone, Copy, Default, Eq, PartialEq)] 389 | pub struct ExtendedFinderFlags(u16); 390 | 391 | impl fmt::Debug for ExtendedFinderFlags { 392 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 393 | let mut flags = vec![]; 394 | if self.are_invalid() { 395 | flags.push("kExtendedFlagsAreInvalid"); 396 | } 397 | if self.has_custom_badge() { 398 | flags.push("kExtendedFlagHasCustomBadge"); 399 | } 400 | if self.has_routing_info() { 401 | flags.push("kExtendedFlagHasCustomBadge"); 402 | } 403 | f.debug_struct("ExtendedFinderFlags") 404 | .field("raw", &self.0) 405 | .field("flags", &flags) 406 | .finish() 407 | } 408 | } 409 | 410 | impl ExtendedFinderFlags { 411 | pub fn are_invalid(&self) -> bool { 412 | self.0 & constants::kExtendedFlagsAreInvalid != 0 413 | } 414 | 415 | pub fn has_custom_badge(&self) -> bool { 416 | self.0 & constants::kExtendedFlagHasCustomBadge != 0 417 | } 418 | 419 | pub fn has_routing_info(&self) -> bool { 420 | self.0 & constants::kExtendedFlagHasRoutingInfo != 0 421 | } 422 | } 423 | 424 | impl From for ExtendedFinderFlags { 425 | fn from(s: u16) -> ExtendedFinderFlags { 426 | ExtendedFinderFlags(s) 427 | } 428 | } 429 | impl From for u16 { 430 | fn from(f: ExtendedFinderFlags) -> u16 { 431 | f.0 432 | } 433 | } 434 | 435 | /// Defines a file information structure. 436 | /// 437 | /// The `FileInfo` structure is preferred over the FInfo structure. 438 | #[derive(Clone, Debug, Default)] 439 | #[repr(C)] 440 | pub struct FileInfo { 441 | /// File type. 442 | pub fileType: OSType, 443 | /// The signature of the application that created the file. 444 | pub fileCreator: OSType, 445 | /// Finder flags. See `FinderFlags`. 446 | pub finderFlags: FinderFlags, 447 | /// The location--specified in coordinates local to the window--of the file's icon within its window. 448 | pub location: Point, 449 | /// The window in which the file's icon appears; this information is meaningful only to the Finder. 450 | pub reservedField: u16, 451 | } 452 | 453 | impl FileInfo { 454 | pub fn read(r: &mut R) -> io::Result { 455 | let mut fileType = [0u8; 4]; 456 | r.read(&mut fileType)?; 457 | let mut fileCreator = [0u8; 4]; 458 | r.read(&mut fileCreator)?; 459 | let finderFlags = r.read_u16::()?.into(); 460 | let location = Point::read(r)?; 461 | let reservedField = r.read_u16::()?; 462 | Ok(FileInfo { 463 | fileType: OSType(fileType), 464 | fileCreator: OSType(fileCreator), 465 | finderFlags, 466 | location, 467 | reservedField, 468 | }) 469 | } 470 | 471 | pub fn write(&self, w: &mut W) -> io::Result<()> { 472 | w.write(&self.fileType.0)?; 473 | w.write(&self.fileCreator.0)?; 474 | w.write_u16::(self.finderFlags.into())?; 475 | self.location.write(w)?; 476 | w.write_u16::(self.reservedField)?; 477 | Ok(()) 478 | } 479 | } 480 | 481 | /// Defines an extended file information structure. 482 | /// 483 | /// The `ExtendedFileInfo` structure is preferred over the FXInfo structure. 484 | #[derive(Clone, Default)] 485 | #[repr(C)] 486 | pub struct ExtendedFileInfo { 487 | /// Reserved (set to 0). 488 | pub reserved1: [i16; 4], 489 | /// Extended flags. See `ExtendedFinderFlags`. 490 | pub extendedFinderFlags: ExtendedFinderFlags, 491 | /// Reserved (set to 0). 492 | pub reserved2: i16, 493 | /// If the user moves the file onto the desktop, the directory ID of the folder from which the 494 | /// user moves the file. 495 | pub putAwayFolderID: i32, 496 | } 497 | 498 | impl fmt::Debug for ExtendedFileInfo { 499 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 500 | if self.reserved1 == [0i16; 4] && self.reserved2 == 0 { 501 | f.debug_struct("ExtendedFileInfo") 502 | .field("extendedFinderFlags", &self.extendedFinderFlags) 503 | .field("putAwayFolderID", &self.putAwayFolderID) 504 | .finish() 505 | } else { 506 | f.debug_struct("ExtendedFileInfo") 507 | .field("reserved1", &self.reserved1) 508 | .field("extendedFinderFlags", &self.extendedFinderFlags) 509 | .field("reserved2", &self.reserved2) 510 | .field("putAwayFolderID", &self.putAwayFolderID) 511 | .finish() 512 | } 513 | } 514 | } 515 | 516 | impl ExtendedFileInfo { 517 | pub fn read(r: &mut R) -> io::Result { 518 | let mut reserved1 = [0i16; 4]; 519 | r.read_i16_into::(&mut reserved1)?; 520 | let extendedFinderFlags = r.read_u16::()?.into(); 521 | let reserved2 = r.read_i16::()?; 522 | let putAwayFolderID = r.read_i32::()?; 523 | Ok(ExtendedFileInfo { 524 | reserved1, 525 | extendedFinderFlags, 526 | reserved2, 527 | putAwayFolderID, 528 | }) 529 | } 530 | 531 | pub fn write(&self, w: &mut W) -> io::Result<()> { 532 | for r in &self.reserved1 { 533 | w.write_i16::(*r)?; 534 | } 535 | w.write_u16::(self.extendedFinderFlags.into())?; 536 | w.write_i16::(self.reserved2)?; 537 | w.write_i32::(self.putAwayFolderID)?; 538 | Ok(()) 539 | } 540 | } 541 | 542 | #[derive(Clone, Debug, Default)] 543 | #[repr(C)] 544 | pub struct FinderInfoFile { 545 | pub file_info: FileInfo, 546 | pub extended_file_info: ExtendedFileInfo, 547 | } 548 | 549 | impl FinderInfoFile { 550 | pub fn read(r: &mut R) -> io::Result { 551 | let file_info = FileInfo::read(r)?; 552 | let extended_file_info = ExtendedFileInfo::read(r)?; 553 | Ok(FinderInfoFile { 554 | file_info, 555 | extended_file_info, 556 | }) 557 | } 558 | 559 | pub fn write(&self, w: &mut W) -> io::Result<()> { 560 | self.file_info.write(w)?; 561 | self.extended_file_info.write(w)?; 562 | Ok(()) 563 | } 564 | } 565 | 566 | /// Defines a directory information structure. 567 | /// 568 | /// The `FolderInfo` structure is preferred over the DInfo structure. 569 | #[derive(Clone, Default)] 570 | #[repr(C)] 571 | pub struct FolderInfo { 572 | /// The rectangle for the window that the Finder displays when the user opens the folder. 573 | pub windowBounds: Rect, 574 | /// Finder flags. See `FinderFlags`. 575 | pub finderFlags: FinderFlags, 576 | /// Location of the folder in the parent window. 577 | pub location: Point, 578 | /// Reserved. Set to 0. 579 | pub reservedField: u16, 580 | } 581 | 582 | impl fmt::Debug for FolderInfo { 583 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 584 | if self.reservedField == 0 { 585 | f.debug_struct("FolderInfo") 586 | .field("windowBounds", &self.windowBounds) 587 | .field("finderFlags", &self.finderFlags) 588 | .field("location", &self.location) 589 | .finish() 590 | } else { 591 | f.debug_struct("FolderInfo") 592 | .field("windowBounds", &self.windowBounds) 593 | .field("finderFlags", &self.finderFlags) 594 | .field("location", &self.location) 595 | .field("reservedField", &self.reservedField) 596 | .finish() 597 | } 598 | } 599 | } 600 | 601 | impl FolderInfo { 602 | pub fn read(r: &mut R) -> io::Result { 603 | let windowBounds = Rect::read(r)?; 604 | let finderFlags = r.read_u16::()?.into(); 605 | let location = Point::read(r)?; 606 | let reservedField = r.read_u16::()?; 607 | Ok(FolderInfo { 608 | windowBounds, 609 | finderFlags, 610 | location, 611 | reservedField, 612 | }) 613 | } 614 | 615 | pub fn write(&self, w: &mut W) -> io::Result<()> { 616 | self.windowBounds.write(w)?; 617 | w.write_u16::(self.finderFlags.into())?; 618 | self.location.write(w)?; 619 | w.write_u16::(self.reservedField)?; 620 | Ok(()) 621 | } 622 | } 623 | 624 | /// Defines an extended directory information structure. 625 | /// 626 | /// The `ExtendedFolderInfo` structure is preferred over the DXInfo structure. 627 | #[derive(Clone, Default)] 628 | #[repr(C)] 629 | pub struct ExtendedFolderInfo { 630 | /// Scroll position within the Finder window. 631 | /// The Finder does not necessarily save this position immediately upon user action. 632 | pub scrollPosition: Point, 633 | /// Reserved (set to 0). 634 | pub reserved1: i32, 635 | /// Extended Finder flags. See `ExtendedFinderFlags`. 636 | pub extendedFinderFlags: ExtendedFinderFlags, 637 | /// Reserved (set to 0). 638 | pub reserved2: i16, 639 | /// If the user moves the folder onto the desktop, the directory ID of the folder from which 640 | /// the user moves it. 641 | pub putAwayFolderID: i32, 642 | } 643 | 644 | impl fmt::Debug for ExtendedFolderInfo { 645 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 646 | if self.reserved1 == 0 && self.reserved2 == 0 { 647 | f.debug_struct("ExtendedFolderInfo") 648 | .field("scrollPosition", &self.scrollPosition) 649 | .field("extendedFinderFlags", &self.extendedFinderFlags) 650 | .field("putAwayFolderID", &self.putAwayFolderID) 651 | .finish() 652 | } else { 653 | f.debug_struct("ExtendedFolderInfo") 654 | .field("scrollPosition", &self.scrollPosition) 655 | .field("reserved1", &self.reserved1) 656 | .field("extendedFinderFlags", &self.extendedFinderFlags) 657 | .field("reserved2", &self.reserved2) 658 | .field("putAwayFolderID", &self.putAwayFolderID) 659 | .finish() 660 | } 661 | } 662 | } 663 | 664 | impl ExtendedFolderInfo { 665 | pub fn read(r: &mut R) -> io::Result { 666 | let scrollPosition = Point::read(r)?; 667 | let reserved1 = r.read_i32::()?; 668 | let extendedFinderFlags = r.read_u16::()?.into(); 669 | let reserved2 = r.read_i16::()?; 670 | let putAwayFolderID = r.read_i32::()?; 671 | Ok(ExtendedFolderInfo { 672 | scrollPosition, 673 | reserved1, 674 | extendedFinderFlags, 675 | reserved2, 676 | putAwayFolderID, 677 | }) 678 | } 679 | 680 | pub fn write(&self, w: &mut W) -> io::Result<()> { 681 | self.scrollPosition.write(w)?; 682 | w.write_i32::(self.reserved1)?; 683 | w.write_u16::(self.extendedFinderFlags.into())?; 684 | w.write_i16::(self.reserved2)?; 685 | w.write_i32::(self.putAwayFolderID)?; 686 | Ok(()) 687 | } 688 | } 689 | 690 | #[derive(Clone, Debug, Default)] 691 | #[repr(C)] 692 | pub struct FinderInfoFolder { 693 | pub folder_info: FolderInfo, 694 | pub extended_folder_info: ExtendedFolderInfo, 695 | } 696 | 697 | impl FinderInfoFolder { 698 | pub fn read(r: &mut R) -> io::Result { 699 | let folder_info = FolderInfo::read(r)?; 700 | let extended_folder_info = ExtendedFolderInfo::read(r)?; 701 | Ok(FinderInfoFolder { 702 | folder_info, 703 | extended_folder_info, 704 | }) 705 | } 706 | 707 | pub fn write(&self, w: &mut W) -> io::Result<()> { 708 | self.folder_info.write(w)?; 709 | self.extended_folder_info.write(w)?; 710 | Ok(()) 711 | } 712 | } 713 | 714 | #[cfg(test)] 715 | mod tests { 716 | use super::*; 717 | 718 | // FinderInfo xattr with custom icon bit off. 719 | const DEFAULT_FINDERINFO_XATTR_VALUE: [u8; 32] = [0u8; 32]; 720 | 721 | // FinderInfo xattr with the custom icon bit on. 722 | const FINDERINFO_XATTR_VALUE_ON: [u8; 32] = [ 723 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 724 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 725 | 0x00, 0x00, 726 | ]; 727 | 728 | // FinderInfo xattr with label = Blue and custom icon bit set. 729 | const FINDERINFO_XATTR_RED_BLUE_FOO_ICON: [u8; 32] = [ 730 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 731 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 732 | 0x00, 0x00, 733 | ]; 734 | 735 | // FinderInfo xattr with label = Red 736 | const FINDERINFO_XATTR_FOO_BLUE_RED: [u8; 32] = [ 737 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 738 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 739 | 0x00, 0x00, 740 | ]; 741 | 742 | // FinderInfo xattr with label = Red with the the custom icon bit set. 743 | const FINDERINFO_XATTR_FOO_BLUE_RED_ICON: [u8; 32] = [ 744 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 745 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 746 | 0x00, 0x00, 747 | ]; 748 | 749 | #[test] 750 | fn test_finderinfo_sizes() { 751 | assert_eq!(::std::mem::size_of::(), 16); 752 | assert_eq!(::std::mem::size_of::(), 16); 753 | assert_eq!(::std::mem::size_of::(), 32); 754 | assert_eq!(::std::mem::size_of::(), 16); 755 | assert_eq!(::std::mem::size_of::(), 16); 756 | assert_eq!(::std::mem::size_of::(), 32); 757 | } 758 | 759 | #[test] 760 | fn test_set_get_finderinfo_file() { 761 | let mut finfo = 762 | FinderInfoFile::read(&mut io::Cursor::new(DEFAULT_FINDERINFO_XATTR_VALUE)).unwrap(); 763 | assert!(!finfo.file_info.finderFlags.has_custom_icon()); 764 | assert_eq!(finfo.file_info.finderFlags.color(), None); 765 | 766 | let mut cursor = io::Cursor::new(vec![]); 767 | finfo.write(&mut cursor).unwrap(); 768 | let serialized = cursor.into_inner(); 769 | assert_eq!(DEFAULT_FINDERINFO_XATTR_VALUE.to_vec(), serialized); 770 | 771 | finfo 772 | .file_info 773 | .finderFlags 774 | .set_color(Some(LabelColor::Blue)); 775 | finfo.file_info.finderFlags.set_has_custom_icon(true); 776 | 777 | let mut cursor = io::Cursor::new(vec![]); 778 | finfo.write(&mut cursor).unwrap(); 779 | let serialized = cursor.into_inner(); 780 | assert_eq!(serialized.len(), 32); 781 | assert_eq!(serialized, FINDERINFO_XATTR_RED_BLUE_FOO_ICON); 782 | 783 | let finfo = 784 | FinderInfoFile::read(&mut io::Cursor::new(FINDERINFO_XATTR_FOO_BLUE_RED_ICON)).unwrap(); 785 | assert!(finfo.file_info.finderFlags.has_custom_icon()); 786 | assert_eq!(finfo.file_info.finderFlags.color(), Some(LabelColor::Red)); 787 | } 788 | 789 | #[test] 790 | fn test_set_get_finderinfo_folder() { 791 | let mut finfo = 792 | FinderInfoFolder::read(&mut io::Cursor::new(DEFAULT_FINDERINFO_XATTR_VALUE)).unwrap(); 793 | assert!(!finfo.folder_info.finderFlags.has_custom_icon()); 794 | assert_eq!(finfo.folder_info.finderFlags.color(), None); 795 | 796 | let mut cursor = io::Cursor::new(vec![]); 797 | finfo.write(&mut cursor).unwrap(); 798 | let serialized = cursor.into_inner(); 799 | assert_eq!(DEFAULT_FINDERINFO_XATTR_VALUE.to_vec(), serialized); 800 | 801 | finfo.folder_info.finderFlags.set_has_custom_icon(true); 802 | 803 | let mut cursor = io::Cursor::new(vec![]); 804 | finfo.write(&mut cursor).unwrap(); 805 | let serialized = cursor.into_inner(); 806 | assert_eq!(serialized.len(), 32); 807 | assert_eq!(serialized, FINDERINFO_XATTR_VALUE_ON); 808 | 809 | finfo 810 | .folder_info 811 | .finderFlags 812 | .set_color(Some(LabelColor::Blue)); 813 | 814 | let mut cursor = io::Cursor::new(vec![]); 815 | finfo.write(&mut cursor).unwrap(); 816 | let serialized = cursor.into_inner(); 817 | assert_eq!(serialized.len(), 32); 818 | assert_eq!(serialized, FINDERINFO_XATTR_RED_BLUE_FOO_ICON); 819 | 820 | let finfo = 821 | FinderInfoFolder::read(&mut io::Cursor::new(FINDERINFO_XATTR_FOO_BLUE_RED)).unwrap(); 822 | assert!(!finfo.folder_info.finderFlags.has_custom_icon()); 823 | assert_eq!(finfo.folder_info.finderFlags.color(), Some(LabelColor::Red)); 824 | } 825 | } 826 | --------------------------------------------------------------------------------