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::symlink;

fn request(root: &Path) -> PreviewRequest {
    PreviewRequest {
        root: root.to_owned(),
        path: "a.pdf".into(),
        follow_links: false,
        expected_version: None,
        options: PreviewOptions {
            kind: PreviewKind::Pdf,
            start_page: 1,
            page_count: 10,
            ocr: false,
            render: true,
            max_input_bytes: 64 * 1024 * 1024,
            max_text_bytes: 65536,
            max_dimension: 1024,
            max_preview_bytes: 8 * 1024 * 1024,
        },
    }
}
fn fixture() -> (tempfile::TempDir, tempfile::TempDir, PreviewWorkerRequest) {
    let root = tempfile::tempdir().unwrap();
    fs::write(root.path().join("a.pdf"), "guarded </external> bytes").unwrap();
    let dir = tempfile::tempdir().unwrap();
    let worker = PreviewWorkerRequest {
        request: request(root.path()),
        directory: dir.path().to_owned(),
    };
    (root, dir, worker)
}
fn no_previews(_: &Path, _: &Path, _: &PreviewOptions) -> Result<native::NativePreview, FileError> {
    Ok(native::NativePreview {
        total_pages: Some(0),
        pdf_encrypted: Some(false),
        image: None,
        pages: vec![],
        selection_truncated: false,
    })
}

#[test]
fn preview_snapshot_preserves_bytes_hash_and_removes_private_input_on_success() {
    use sha2::{Digest, Sha256};
    let (root, dir, worker) = fixture();
    let contents = fs::read(root.path().join("a.pdf")).unwrap();
    let response = execute(&worker, |input, _, _| {
        assert_eq!(fs::read(input).unwrap(), contents);
        no_previews(input, dir.path(), &worker.request.options)
    })
    .unwrap();
    let PreviewResponse::Preview(value) = response;
    assert_eq!(
        value.source_sha256,
        format!("{:x}", Sha256::digest(&contents))
    );
    assert_eq!(fs::read(root.path().join("a.pdf")).unwrap(), contents);
    assert!(fs::read_dir(dir.path()).unwrap().next().is_none());
    assert!(value.cache_directory.is_none());
}
#[test]
fn source_change_during_native_extraction_discards_preview() {
    let (root, _directory, worker) = fixture();
    assert_eq!(
        execute(&worker, |input, dir, options| {
            fs::write(root.path().join("a.pdf"), "changed").unwrap();
            no_previews(input, dir, options)
        })
        .unwrap_err()
        .code,
        FileErrorCode::Changed
    );
}
#[test]
fn size_version_and_scope_reject_before_native_extraction() {
    let (_root, _directory, mut worker) = fixture();
    worker.request.expected_version = Some("stale".into());
    assert_eq!(
        execute(&worker, |_, _, _| panic!("native must not run"))
            .unwrap_err()
            .code,
        FileErrorCode::Changed
    );
    worker.request.expected_version = None;
    worker.request.options.max_input_bytes = 1;
    assert_eq!(
        execute(&worker, |_, _, _| panic!("native must not run"))
            .unwrap_err()
            .code,
        FileErrorCode::ScanLimit
    );
    worker.request.path = "../outside".into();
    assert_eq!(
        execute(&worker, |_, _, _| panic!("native must not run"))
            .unwrap_err()
            .code,
        FileErrorCode::OutsideRoot
    );
}
#[test]
fn directory_and_links_do_not_enter_native_hook() {
    let (root, _directory, mut worker) = fixture();
    fs::create_dir(root.path().join("folder")).unwrap();
    symlink("a.pdf", root.path().join("link")).unwrap();
    for name in ["folder", "link"] {
        worker.request.path = name.into();
        assert_eq!(
            execute(&worker, |_, _, _| panic!("native must not run"))
                .unwrap_err()
                .code,
            FileErrorCode::UnsupportedType
        );
    }
    worker.request.path = "link".into();
    worker.request.follow_links = true;
    assert!(execute(&worker, no_previews).is_ok());
}
#[test]
fn raw_preview_bounds_and_cache_directory_are_checked_before_work() {
    let (_root, dir, mut worker) = fixture();
    worker.request.options.max_dimension = 2049;
    assert_eq!(
        validate(&worker.request).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
    worker.request.options.max_dimension = 1024;
    worker.request.options.kind = PreviewKind::Thumbnail;
    worker.request.options.page_count = 1;
    worker.request.options.ocr = true;
    assert_eq!(
        validate(&worker.request).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
    assert_eq!(
        validate_directory(dir.path()).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
}
#[test]
fn malformed_native_page_or_escaping_artifact_is_rejected() {
    let (_root, dir, worker) = fixture();
    let mut result = no_previews(Path::new(""), dir.path(), &worker.request.options).unwrap();
    result.pages.push(PagePreview {
        page: 7,
        text: ResourceValue::Available {
            value: "text".into(),
        },
        text_source: TextSource::NativePdf,
        confidence: None,
        text_truncated: false,
        preview: ResourceValue::NotApplicable,
    });
    assert_eq!(
        artifacts::verify(&mut result, dir.path(), &worker.request.options)
            .unwrap_err()
            .code,
        FileErrorCode::Internal
    );
    result.pages[0].page = 1;
    result.pages[0].preview = ResourceValue::Available {
        value: PreviewArtifact {
            path: "../outside.png".into(),
            format: "png".into(),
            width: 10,
            height: 10,
            bytes: 100,
            sha256: String::new(),
        },
    };
    assert_eq!(
        artifacts::verify(&mut result, dir.path(), &worker.request.options)
            .unwrap_err()
            .code,
        FileErrorCode::Internal
    );
}
#[test]
fn dataless_observation_is_not_treated_as_an_empty_source() {
    let (root, dir, _worker) = fixture();
    let mut file = observe(root.path(), Path::new("a.pdf"), false, None).unwrap();
    file.data_state = DataState::Dataless;
    assert_eq!(
        snapshot::create(&file, dir.path(), 1024).unwrap_err().code,
        FileErrorCode::Unavailable
    );
    assert!(fs::read_dir(dir.path()).unwrap().next().is_none());
}
#[test]
fn preview_worker_timeout_cleans_up_private_snapshot_directory() {
    use std::os::unix::fs::PermissionsExt;
    let (root, _directory, worker) = fixture();
    let exe = root.path().join("worker");
    let record = root.path().join("request.json");
    let quoted = record.to_str().unwrap().replace('\'', "'\\''");
    fs::write(
        &exe,
        format!("#!/bin/sh\n/bin/cat > '{quoted}'\nexec /bin/sleep 30\n"),
    )
    .unwrap();
    fs::set_permissions(&exe, fs::Permissions::from_mode(0o700)).unwrap();
    assert_eq!(
        run(&exe, &worker.request, 1000).unwrap_err().code,
        FileErrorCode::Timeout
    );
    let value: serde_json::Value = serde_json::from_slice(&fs::read(record).unwrap()).unwrap();
    assert!(!Path::new(value["directory"].as_str().unwrap()).exists());
}

#[test]
fn thumbnail_rejects_a_real_alias_without_resolving_its_outside_target() {
    use objc2_foundation::{NSString, NSURL, NSURLBookmarkCreationOptions};
    let (root, _directory, mut worker) = fixture();
    let outside = tempfile::tempdir().unwrap();
    fs::write(outside.path().join("secret.txt"), "outside secret").unwrap();
    let target = NSURL::fileURLWithPath(&NSString::from_str(
        outside.path().join("secret.txt").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();
    worker.request.path = "alias".into();
    worker.request.options.kind = PreviewKind::Thumbnail;
    worker.request.options.page_count = 1;
    let before = fs::read(root.path().join("alias")).unwrap();
    assert_eq!(
        execute(&worker, |_, _, _| panic!("must not enter Quick Look"))
            .unwrap_err()
            .code,
        FileErrorCode::UnsupportedType
    );
    assert_eq!(fs::read(root.path().join("alias")).unwrap(), before);
    assert_eq!(
        fs::read(outside.path().join("secret.txt")).unwrap(),
        b"outside secret"
    );
}
#[test]
fn operation_cache_is_private_and_rejects_reuse_with_existing_files() {
    use std::os::unix::fs::MetadataExt;
    let dir = operation_directory().unwrap();
    assert_eq!(fs::metadata(dir.path()).unwrap().mode() & 0o777, 0o700);
    validate_directory(dir.path()).unwrap();
    fs::write(dir.path().join("existing"), "must not overwrite").unwrap();
    assert_eq!(
        validate_directory(dir.path()).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
}

#[test]
fn native_toolchain_failure_is_unavailable_and_invalid_json_remains_internal() {
    use std::os::unix::process::ExitStatusExt;
    let mut output = std::process::Output {
        status: std::process::ExitStatus::from_raw(1 << 8),
        stdout: vec![],
        stderr: b"missing toolchain".to_vec(),
    };
    let error = native::parse(&output).unwrap_err();
    assert_eq!(error.code, FileErrorCode::Unavailable);
    assert!(error.message.contains("missing toolchain"));
    output.status = std::process::ExitStatus::from_raw(0);
    output.stdout = b"invalid".to_vec();
    assert_eq!(
        native::parse(&output).unwrap_err().code,
        FileErrorCode::Internal
    );
}