cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::tests::Fixture;
use super::*;
use std::fs;
use std::os::unix::fs::symlink;

pub(super) fn options() -> ReadOptions {
    ReadOptions {
        offset: 0,
        max_bytes: 65536,
        encoding: FileEncoding::Utf8,
        start_line: None,
        line_count: 100,
    }
}

fn read(f: &Fixture, opts: ReadOptions) -> Result<FileRead, FileError> {
    let FileResponse::Read(value) = f.run("a", FileAction::Read(opts))? else {
        panic!("read")
    };
    Ok(value)
}

#[test]
fn utf8_byte_ranges_resume_without_splitting_characters() {
    let f = Fixture::new();
    f.write("a", "A臺灣B");
    let mut opts = options();
    opts.max_bytes = 5;
    let first = read(&f, opts.clone()).unwrap();
    assert_eq!(first.content, "A臺");
    assert_eq!(first.bytes_read, 4);
    assert_eq!(first.next_offset, Some(4));
    assert!(first.truncated);
    opts.offset = 4;
    let second = read(&f, opts.clone()).unwrap();
    assert_eq!(second.content, "灣B");
    assert!(second.eof);
    assert!(!second.truncated);
    opts.offset = 8;
    let end = read(&f, opts.clone()).unwrap();
    assert_eq!(end.content, "");
    assert_eq!(end.bytes_read, 0);
    assert!(end.eof);
    opts.offset = 2;
    assert_eq!(read(&f, opts).unwrap_err().code, FileErrorCode::DecodeError);
    let mut opts = options();
    opts.offset = 1;
    opts.max_bytes = 2;
    assert_eq!(
        read(&f, opts).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
}

#[test]
fn strict_decoding_reports_binary_invalid_and_incomplete_text() {
    let f = Fixture::new();
    for (bytes, code) in [
        (vec![0, 65], FileErrorCode::BinaryData),
        (vec![0xff], FileErrorCode::DecodeError),
        (vec![0xe8, 0x87], FileErrorCode::DecodeError),
    ] {
        f.write("a", bytes);
        assert_eq!(read(&f, options()).unwrap_err().code, code);
    }
    let mut opts = options();
    opts.encoding = FileEncoding::Hex;
    assert_eq!(read(&f, opts).unwrap().content, "e887");
    f.write("a", "");
    assert!(read(&f, options()).unwrap().eof);
}

#[test]
fn utf16_both_endiannesses_resume_surrogate_pairs_and_reject_invalid_ranges() {
    let f = Fixture::new();
    for encoding in [FileEncoding::Utf16Le, FileEncoding::Utf16Be] {
        let bytes: Vec<u8> = "A𠀀B"
            .encode_utf16()
            .flat_map(|unit| {
                if encoding == FileEncoding::Utf16Le {
                    unit.to_le_bytes()
                } else {
                    unit.to_be_bytes()
                }
            })
            .collect();
        f.write("a", bytes);
        let mut opts = options();
        opts.encoding = encoding;
        opts.max_bytes = 5;
        let first = read(&f, opts.clone()).unwrap();
        assert_eq!(first.content, "A");
        assert_eq!(first.next_offset, Some(2));
        opts.offset = 2;
        opts.max_bytes = 6;
        assert_eq!(read(&f, opts.clone()).unwrap().content, "𠀀B");
        opts.offset = 1;
        assert_eq!(
            read(&f, opts).unwrap_err().code,
            FileErrorCode::InvalidOptions
        );
    }
    for bytes in [vec![65], vec![0, 0xd8], vec![0, 0xdc]] {
        f.write("a", bytes);
        let mut opts = options();
        opts.encoding = FileEncoding::Utf16Le;
        assert_eq!(read(&f, opts).unwrap_err().code, FileErrorCode::DecodeError);
    }
}

#[test]
fn line_reads_preserve_crlf_and_report_partial_line_byte_resume() {
    let f = Fixture::new();
    f.write("a", "one\r\n臺灣\nlast");
    let mut opts = options();
    opts.start_line = Some(2);
    opts.line_count = 1;
    let second = read(&f, opts.clone()).unwrap();
    assert_eq!(second.content, "臺灣\n");
    assert_eq!(second.offset, 5);
    assert_eq!(second.lines_complete, Some(1));
    assert_eq!(second.next_line, Some(3));
    assert_eq!(second.next_offset, Some(12));
    opts.max_bytes = 4;
    let partial = read(&f, opts.clone()).unwrap();
    assert_eq!(partial.content, "臺");
    assert_eq!(partial.next_offset, Some(8));
    assert_eq!(partial.lines_complete, Some(0));
    assert_eq!(partial.next_line, Some(2));
    opts.start_line = Some(3);
    opts.max_bytes = 100;
    let last = read(&f, opts.clone()).unwrap();
    assert_eq!(last.content, "last");
    assert!(last.eof);
    assert_eq!(last.lines_complete, Some(1));
    opts.start_line = Some(4);
    assert_eq!(
        read(&f, opts).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
    f.write("a", "");
    let mut opts = options();
    opts.start_line = Some(1);
    assert!(read(&f, opts).unwrap().eof);
}

#[test]
fn line_prefix_is_bounded_and_invalid_limits_are_rejected() {
    let f = Fixture::new();
    f.write("a", vec![b'a'; MAX_SCAN_BYTES as usize + 1]);
    let mut opts = options();
    opts.start_line = Some(2);
    assert_eq!(read(&f, opts).unwrap_err().code, FileErrorCode::ScanLimit);
    for opts in [
        ReadOptions {
            max_bytes: 0,
            ..options()
        },
        ReadOptions {
            max_bytes: MAX_READ_BYTES + 1,
            ..options()
        },
        ReadOptions {
            offset: MAX_SCAN_BYTES + 2,
            ..options()
        },
        ReadOptions {
            start_line: Some(0),
            ..options()
        },
        ReadOptions {
            start_line: Some(1),
            offset: 1,
            ..options()
        },
        ReadOptions {
            start_line: Some(1),
            encoding: FileEncoding::Hex,
            ..options()
        },
        ReadOptions {
            start_line: Some(1),
            line_count: 0,
            ..options()
        },
        ReadOptions {
            start_line: Some(1),
            line_count: 10001,
            ..options()
        },
    ] {
        assert_eq!(
            read(&f, opts).unwrap_err().code,
            FileErrorCode::InvalidOptions
        );
    }
}

#[test]
fn refuses_special_files_and_links_before_reading() {
    let f = Fixture::new();
    fs::create_dir(f.0.path().join("a")).unwrap();
    assert_eq!(
        read(&f, options()).unwrap_err().code,
        FileErrorCode::UnsupportedType
    );
    fs::remove_dir(f.0.path().join("a")).unwrap();
    symlink("missing", f.0.path().join("a")).unwrap();
    assert_eq!(
        read(&f, options()).unwrap_err().code,
        FileErrorCode::SymlinkDisallowed
    );
    fs::remove_file(f.0.path().join("a")).unwrap();
    let status = std::process::Command::new("/usr/bin/mkfifo")
        .arg(f.0.path().join("a"))
        .status()
        .unwrap();
    assert!(status.success());
    assert_eq!(
        read(&f, options()).unwrap_err().code,
        FileErrorCode::UnsupportedType
    );
}

#[test]
fn changed_open_identity_discards_data_and_stale_version_is_rejected() {
    struct Replace;
    impl FilePlatform for Replace {
        fn stamp(&self, metadata: &Metadata) -> FileStamp {
            MacFiles.stamp(metadata)
        }
        fn open_regular(&self, path: &Path) -> io::Result<File> {
            fs::rename(path, path.with_extension("old"))?;
            fs::write(path, b"replacement secret")?;
            MacFiles.open_regular(path)
        }
    }
    let f = Fixture::new();
    f.write("a", "original");
    let request = f.request("a", FileAction::Read(options()));
    assert_eq!(
        cueward_core::files::execute(&Replace, &request)
            .unwrap_err()
            .code,
        FileErrorCode::Changed
    );
    let FileResponse::Info(info) = f.run("a", FileAction::Info).unwrap() else {
        panic!("info")
    };
    let mut request = request;
    request.expected_version = Some(info.version);
    f.write("a", "modified");
    assert_eq!(
        execute_worker(&request).unwrap_err().code,
        FileErrorCode::Changed
    );
}