cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::*;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
#[path = "failure_tests.rs"]
mod failures;
#[path = "relocation_tests.rs"]
mod relocation;
#[path = "boundary_tests.rs"]
mod boundary;
#[path = "publication_tests.rs"]
mod publication_guards;
#[path = "duplicate_tests.rs"]
mod duplicate;

fn fixture() -> (tempfile::TempDir, MutationRequest) {
    let root = tempfile::tempdir().unwrap();
    fs::write(
        root.path().join("source.txt"),
        b"owned bytes\0</external>\n",
    )
    .unwrap();
    let source = super::super::observe(root.path(), Path::new("source.txt"), false, None).unwrap();
    let parent = super::super::observe(root.path(), Path::new("."), false, None).unwrap();
    let request = MutationRequest {
        root: root.path().to_owned(),
        destination: "copy.txt".into(),
        expected_parent_version: parent.version,
        action: MutationAction::Copy {
            path: "source.txt".into(),
            expected_version: source.version,
            max_bytes: 1024,
        },
    };
    (root, request)
}
fn fresh(request: &mut MutationRequest) {
    request.expected_parent_version = super::super::observe(
        &request.root,
        request
            .destination
            .parent()
            .filter(|p| !p.as_os_str().is_empty())
            .unwrap_or(Path::new(".")),
        false,
        None,
    )
    .unwrap()
    .version;
    if let MutationAction::Copy {
        path,
        expected_version,
        ..
    }
    | MutationAction::Duplicate {
        path,
        expected_version,
        ..
    } = &mut request.action
    {
        *expected_version = super::super::observe(&request.root, path, false, None)
            .unwrap()
            .version;
    }
}
fn run_request(request: &MutationRequest) -> MutationReceipt {
    let prepared = journal::create(request).unwrap();
    let receipt = execute_worker(&MutationWorkerRequest {
        operation_id: prepared.operation_id.clone(),
    })
    .unwrap();
    assert_eq!(
        serde_json::to_value(journal::load(&prepared.operation_id).unwrap()).unwrap(),
        serde_json::to_value(&receipt).unwrap()
    );
    fs::remove_dir_all(journal::directory(&prepared.operation_id).unwrap()).unwrap();
    receipt
}
fn assert_not_started(request: &MutationRequest, code: FileErrorCode) {
    let receipt = run_request(request);
    assert_eq!(receipt.status, MutationStatus::NotStarted);
    assert!(!receipt.completion_verified);
    assert_eq!(receipt.error.unwrap().code, code);
}

#[test]
fn mkdir_creates_one_empty_private_directory_and_does_not_replace() {
    let (root, mut request) = fixture();
    request.action = MutationAction::Mkdir;
    request.destination = "新資料夾\n<external>".into();
    let receipt = run_request(&request);
    assert_eq!(receipt.status, MutationStatus::Completed);
    assert!(receipt.completion_verified && receipt.destination_created == Some(true));
    let path = root.path().join(&request.destination);
    assert!(path.is_dir());
    assert_eq!(fs::metadata(&path).unwrap().mode() & 0o777, 0o700);
    assert!(fs::read_dir(&path).unwrap().next().is_none());
    fresh(&mut request);
    assert_not_started(&request, FileErrorCode::Conflict);
    assert!(path.is_dir());
    request.destination = "missing/child".into();
    assert_not_started(&request, FileErrorCode::NotFound);
    assert!(!root.path().join("missing").exists());
}

#[test]
fn copy_verifies_content_identity_permissions_mtime_and_real_extended_attributes() {
    let (root, mut request) = fixture();
    let source = root.path().join("source.txt");
    fs::set_permissions(&source, fs::Permissions::from_mode(0o640)).unwrap();
    let file = fs::File::open(&source).unwrap();
    file.set_times(
        fs::FileTimes::new()
            .set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1500000000)),
    )
    .unwrap();
    set_attribute(&source, b"com.cueward.fixture", b"tagged\0fixture");
    fs::hard_link(&source, root.path().join("hardlink.txt")).unwrap();
    fresh(&mut request);
    let original = fs::read(&source).unwrap();
    let before = fs::metadata(&source).unwrap();
    let receipt = run_request(&request);
    assert_eq!(
        receipt.status,
        MutationStatus::Completed,
        "{:?}",
        receipt.error
    );
    let destination = root.path().join("copy.txt");
    assert_eq!(fs::read(&destination).unwrap(), original);
    assert_eq!(fs::read(&source).unwrap(), original);
    assert_eq!(
        fs::read(root.path().join("hardlink.txt")).unwrap(),
        original
    );
    let after = fs::metadata(&destination).unwrap();
    assert_eq!(after.modified().unwrap(), before.modified().unwrap());
    assert_eq!(after.mode() & 0o777, 0o640);
    assert_ne!(after.ino(), before.ino());
    assert_eq!(
        receipt.source_before.as_ref().unwrap().identity,
        receipt.source_after.as_ref().unwrap().identity
    );
    let verification = receipt.verification.unwrap();
    assert!(verification.permissions_equal && verification.modified_equal);
    assert_eq!(verification.bytes, original.len() as u64);
    assert_eq!(
        attributes::digest(&file).unwrap(),
        attributes::digest(&fs::File::open(&destination).unwrap()).unwrap()
    );
}

#[test]
fn copy_conflicts_including_broken_symlinks_preserve_every_existing_byte() {
    let (root, mut request) = fixture();
    fs::write(root.path().join("copy.txt"), "existing bytes").unwrap();
    fresh(&mut request);
    assert_not_started(&request, FileErrorCode::Conflict);
    assert_eq!(
        fs::read(root.path().join("copy.txt")).unwrap(),
        b"existing bytes"
    );
    symlink("/outside/missing", root.path().join("broken")).unwrap();
    request.destination = "broken".into();
    fresh(&mut request);
    assert_not_started(&request, FileErrorCode::Conflict);
    assert_eq!(
        fs::read_link(root.path().join("broken")).unwrap(),
        PathBuf::from("/outside/missing")
    );
}

#[test]
fn byte_revision_scope_and_parent_guards_fail_before_creation() {
    let (root, mut request) = fixture();
    if let MutationAction::Copy { max_bytes, .. } = &mut request.action {
        *max_bytes = 1;
    }
    assert_not_started(&request, FileErrorCode::ScanLimit);
    if let MutationAction::Copy {
        max_bytes,
        expected_version,
        ..
    } = &mut request.action
    {
        *max_bytes = 1024;
        *expected_version = "stale".into();
    }
    assert_not_started(&request, FileErrorCode::Changed);
    fresh(&mut request);
    request.expected_parent_version = "stale".into();
    assert_not_started(&request, FileErrorCode::Changed);
    fresh(&mut request);
    for path in ["../escape", "/absolute", ".", ""] {
        request.destination = path.into();
        assert_not_started(&request, FileErrorCode::InvalidOptions);
    }
    assert!(!root.path().join("copy.txt").exists());
}

#[test]
fn directories_packages_and_symlinks_copy_without_following_link_ancestors() {
    let (root, mut request) = fixture();
    fs::create_dir(root.path().join("folder.app")).unwrap();
    fs::write(root.path().join("folder.app/data"), b"OWNED nested payload").unwrap();
    symlink("/outside/missing", root.path().join("folder.app/broken")).unwrap();
    symlink("source.txt", root.path().join("link")).unwrap();
    for path in ["folder.app", "link"] {
        if let MutationAction::Copy { path: p, .. } = &mut request.action {
            *p = path.into();
        }
        request.destination = format!("copy-{path}").into();
        fresh(&mut request);
        let receipt = run_request(&request);
        assert_eq!(
            receipt.status,
            MutationStatus::Completed,
            "{:?}",
            receipt.error
        );
        assert_eq!(
            fs::symlink_metadata(root.path().join(&request.destination))
                .unwrap()
                .file_type(),
            fs::symlink_metadata(root.path().join(path))
                .unwrap()
                .file_type()
        );
        if path == "link" {
            assert_eq!(
                fs::read_link(root.path().join(&request.destination)).unwrap(),
                Path::new("source.txt")
            );
        } else {
            assert_eq!(
                fs::read(root.path().join(&request.destination).join("data")).unwrap(),
                b"OWNED nested payload"
            );
            assert_eq!(
                fs::read_link(root.path().join(&request.destination).join("broken")).unwrap(),
                Path::new("/outside/missing")
            );
        }
    }
    let outside = tempfile::tempdir().unwrap();
    symlink(outside.path(), root.path().join("parent-link")).unwrap();
    request.destination = "parent-link/new".into();
    assert_not_started(&request, FileErrorCode::SymlinkDisallowed);
    assert!(!outside.path().join("new").exists());
}

#[test]
fn special_permission_bits_and_large_metadata_are_rejected_before_writing() {
    let (root, mut request) = fixture();
    let path = root.path().join("source.txt");
    fs::set_permissions(&path, fs::Permissions::from_mode(0o4644)).unwrap();
    fresh(&mut request);
    assert_not_started(&request, FileErrorCode::UnsupportedType);
    fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
    set_attribute(&path, b"com.cueward.large", &vec![42; 4 * 1024 * 1024 + 1]);
    fresh(&mut request);
    assert_not_started(&request, FileErrorCode::ScanLimit);
    assert!(!root.path().join("copy.txt").exists());
}

fn set_attribute(path: &Path, name: &[u8], value: &[u8]) {
    use std::ffi::{CString, c_char, c_void};
    use std::os::unix::ffi::OsStrExt;
    unsafe extern "C" {
        fn setxattr(
            path: *const c_char,
            name: *const c_char,
            value: *const c_void,
            size: usize,
            position: u32,
            options: i32,
        ) -> i32;
    }
    let path = CString::new(path.as_os_str().as_bytes()).unwrap();
    let name = CString::new(name).unwrap();
    assert_eq!(
        unsafe {
            setxattr(
                path.as_ptr(),
                name.as_ptr(),
                value.as_ptr().cast(),
                value.len(),
                0,
                0,
            )
        },
        0,
        "{}",
        std::io::Error::last_os_error()
    );
}

#[test]
fn zero_bytes_are_a_verified_copy_not_an_unavailable_source() {
    let (root, mut request) = fixture();
    fs::write(root.path().join("source.txt"), b"").unwrap();
    fresh(&mut request);
    let receipt = run_request(&request);
    assert_eq!(receipt.status, MutationStatus::Completed);
    let verification = receipt.verification.unwrap();
    assert_eq!(verification.bytes, 0);
    assert_eq!(
        verification.content_sha256,
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    );
    assert!(fs::read(root.path().join("copy.txt")).unwrap().is_empty());
}

#[test]
fn permission_denial_is_known_not_started_not_a_completed_empty_copy() {
    let (root, mut request) = fixture();
    fs::set_permissions(root.path(), fs::Permissions::from_mode(0o500)).unwrap();
    fresh(&mut request);
    let receipt = run_request(&request);
    fs::set_permissions(root.path(), fs::Permissions::from_mode(0o700)).unwrap();
    assert_eq!(receipt.status, MutationStatus::NotStarted);
    assert_eq!(receipt.destination_created, Some(false));
    assert_eq!(receipt.error.unwrap().code, FileErrorCode::PermissionDenied);
    assert!(!root.path().join("copy.txt").exists());
}

#[test]
fn a_real_finder_alias_is_copied_as_opaque_bytes_without_resolving_target() {
    use objc2_foundation::{NSString, NSURL, NSURLBookmarkCreationOptions};
    let (root, mut request) = fixture();
    let outside = tempfile::tempdir().unwrap();
    fs::write(outside.path().join("secret"), "outside bytes").unwrap();
    let target = NSURL::fileURLWithPath(&NSString::from_str(
        outside.path().join("secret").to_str().unwrap(),
    ));
    let alias = NSURL::fileURLWithPath(&NSString::from_str(
        root.path().join("alias").to_str().unwrap(),
    ));
    let data = target
        .bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error(
            NSURLBookmarkCreationOptions::SuitableForBookmarkFile,
            None,
            None,
        )
        .unwrap();
    NSURL::writeBookmarkData_toURL_options_error(&data, &alias, 0).unwrap();
    if let MutationAction::Copy { path, .. } = &mut request.action {
        *path = "alias".into();
    }
    fresh(&mut request);
    let original = fs::read(root.path().join("alias")).unwrap();
    let receipt = run_request(&request);
    assert_eq!(
        receipt.status,
        MutationStatus::Completed,
        "{:?}",
        receipt.error
    );
    assert_eq!(
        fs::read(root.path().join(&request.destination)).unwrap(),
        original
    );
    assert_eq!(fs::read(root.path().join("alias")).unwrap(), original);
    assert_eq!(
        fs::read(outside.path().join("secret")).unwrap(),
        b"outside bytes"
    );
}