//! `toolkit read_file` and `toolkit write_file`, run as `brokerd` runs them. Do not edit. mod support; use std::os::unix::fs::PermissionsExt; use proto::tools::{ReadFileArgs, WriteFileArgs}; use support::{TempDir, json, toolkit}; use toolkit::files::MAX_READ; fn read(path: &str) -> support::Ran { toolkit( &["read_file"], &json(&ReadFileArgs { path: path.to_string(), }), ) } fn write(path: &str, content: &str) -> support::Ran { let args = WriteFileArgs { path: path.to_string(), content: content.to_string(), }; toolkit(&["write_file"], &json(&args)) } #[test] fn a_file_is_read_exactly() { let dir = TempDir::new("read"); let text = "line one\nline two, no newline at the end: ✓"; std::fs::write(dir.at("a.md"), text).unwrap(); let ran = read(&dir.at("a.md")); assert_eq!((ran.code, ran.stdout.as_str()), (0, text)); assert_eq!(ran.stderr, ""); } #[test] fn an_empty_file_is_empty_output() { let dir = TempDir::new("read-empty"); std::fs::write(dir.at("e"), "").unwrap(); let ran = read(&dir.at("e")); assert_eq!((ran.code, ran.stdout.as_str()), (0, "")); } #[test] fn what_cannot_be_read_is_exit_1_with_one_line_for_the_model() { let dir = TempDir::new("read-bad"); std::fs::write(dir.at("bin"), [0xff, 0xfe, 0x00]).unwrap(); std::fs::write(dir.at("big"), vec![b'a'; MAX_READ + 1]).unwrap(); std::fs::write(dir.at("exact"), vec![b'a'; MAX_READ]).unwrap(); let cases = [ (dir.at("missing"), "no such file"), (dir.at(""), "is a directory"), (dir.at("bin"), "not UTF-8 text"), (dir.at("big"), "larger than 1048576 bytes"), ]; for (path, why) in cases { let ran = read(&path); assert_eq!(ran.code, 1, "{path}"); assert_eq!(ran.stdout, format!("read_file: {path}: {why}")); } let ran = read(&dir.at("exact")); assert_eq!( (ran.code, ran.stdout.len()), (0, MAX_READ), "exactly the limit is fine" ); } #[test] fn an_unreadable_file_names_the_error() { if is_root() { return; // root reads anything } let dir = TempDir::new("read-perm"); std::fs::write(dir.at("secret"), "x").unwrap(); std::fs::set_permissions(dir.at("secret"), std::fs::Permissions::from_mode(0o000)).unwrap(); let ran = read(&dir.at("secret")); assert_eq!(ran.code, 1); assert!( ran.stdout .starts_with(&format!("read_file: {}: ", dir.at("secret"))), "{}", ran.stdout ); assert!(ran.stdout.contains("ermission denied"), "{}", ran.stdout); } #[test] fn a_file_is_written_created_or_replaced() { let dir = TempDir::new("write"); let ran = write(&dir.at("new.md"), "hello ✓\n"); assert_eq!(ran.code, 0); assert_eq!( ran.stdout, format!("wrote 10 bytes to {}", dir.at("new.md")) ); assert_eq!( std::fs::read_to_string(dir.at("new.md")).unwrap(), "hello ✓\n" ); let ran = write(&dir.at("new.md"), ""); assert_eq!(ran.stdout, format!("wrote 0 bytes to {}", dir.at("new.md"))); assert_eq!(std::fs::read_to_string(dir.at("new.md")).unwrap(), ""); } #[test] fn what_cannot_be_written_is_exit_1() { let dir = TempDir::new("write-bad"); std::fs::create_dir(dir.at("sub")).unwrap(); let cases = [ (dir.at("nope/a.md"), "the directory does not exist"), (dir.at("sub"), "is a directory"), ]; for (path, why) in cases { let ran = write(&path, "x"); assert_eq!(ran.code, 1, "{path}"); assert_eq!(ran.stdout, format!("write_file: {path}: {why}")); } assert!(!dir.path().join("nope").exists(), "no directory is created"); } #[test] fn misuse_is_exit_2_with_nothing_on_standard_output() { let long = vec![b' '; toolkit::input::MAX_INPUT + 1]; let cases: [(&[&str], &[u8], &str); 7] = [ ( &["read_file"], br#"{"path":"/a","mode":1}"#, "the arguments do not parse", ), (&["read_file"], b"", "the arguments do not parse"), ( &["write_file"], br#"{"path":"/a"}"#, "the arguments do not parse", ), (&["read_file"], &[0xff, 0xfe], "not UTF-8"), (&["read_file"], &long, "larger than 2097152 bytes"), (&["format_disk"], b"{}", "unknown tool"), (&[], b"{}", "unknown tool"), ]; for (args, input, why) in cases { let ran = toolkit(args, input); assert_eq!(ran.code, 2, "{args:?}"); assert_eq!( ran.stdout, "", "{args:?}: the model sees nothing of a misuse" ); assert!(ran.stderr.contains(why), "{args:?}: {}", ran.stderr); } } fn is_root() -> bool { std::fs::read_to_string("/proc/self/status") .map(|s| s.lines().any(|l| l.starts_with("Uid:\t0\t"))) .unwrap_or(false) }