exclude_from_backups 0.1.0

Mark files or directories as excluded from backups (for Time Machine on macOS) Can be used to prevent caches and temporary files from bloating backups.
Documentation
use std::path::Path;
use std::ffi::CString;
use std::ptr;
use objc::runtime as objc;
use error::Error;
use error::Error::*;
use std::os::raw::c_void;

#[link(kind="framework", name="Cocoa")]
extern "C" {
    #[no_mangle]
    static NSURLIsExcludedFromBackupKey: *const c_void;
}

pub fn exclude_from_backups<P: AsRef<Path>>(path: P) -> Result<(), Error> {
    let path_cstr = CString::new(path.as_ref().to_str().ok_or(IncompatiblePathCharset)?)
        .map_err(|_|IncompatiblePathCharset)?;
    unsafe {
        let nsstring_cls = objc::Class::get("NSString").ok_or(ObjcCallFailed)?;
        let nsnumber_cls = objc::Class::get("NSNumber").ok_or(ObjcCallFailed)?;
        let nsurl_cls = objc::Class::get("NSURL").ok_or(ObjcCallFailed)?;

        // [NSString stringWithUTF8String:char *]
        let path_nsstr: *mut objc::Object = msg_send![nsstring_cls, stringWithUTF8String:path_cstr.as_ptr()];
        if path_nsstr.is_null() {
            return Err(ObjcCallFailed);
        }

        // @YES
        let yes_nsnumber: *mut objc::Object = msg_send![nsnumber_cls, numberWithBool:objc::YES];
        if yes_nsnumber.is_null() {
            return Err(ObjcCallFailed);
        }

        // url = [NSURL fileURLWithPath:root isDirectory:YES]
        let url: *mut objc::Object = msg_send![nsurl_cls, fileURLWithPath:path_nsstr isDirectory:objc::YES];
        if url.is_null() {
            return Err(ObjcCallFailed);
        }

        // [url setResourceValue:@YES forKey: NSURLIsExcludedFromBackupKey error:nil];
        let nil_err: *mut *mut objc::Object = ptr::null_mut();
        let res: objc::BOOL = msg_send![url, setResourceValue:yes_nsnumber forKey:NSURLIsExcludedFromBackupKey error:nil_err];
        if res != objc::YES {
            return Err(SystemCallFailed);
        }
    }
    Ok(())
}

#[test]
fn test_excluding() {
    use std::fs;
    let path = Path::new("/tmp/exclude_from_backups_test_dir");
    if !path.exists() {
        fs::create_dir(path).unwrap();
    }
    exclude_from_backups(&path).unwrap();
    let _ = fs::remove_dir_all(path);
}