use std::collections::BTreeMap;
use fslite_command::CommandOutput;
use fslite_command::render::{
render_human, render_json, sanitize_for_terminal, sanitize_name, sanitize_preview,
};
use fslite_core::{
ByteRange, Change, ChangeKind, LinkTarget, Node, NodeId, NodeKind, Page, Revision, SearchMatch,
TrashEntry, TrashId, TreeEntry, VirtualPath, WorkspaceId,
};
fn sample_node(name: &str) -> Node {
Node {
workspace_id: WorkspaceId::new(),
id: NodeId::new(),
parent_id: None,
name: name.to_string(),
kind: NodeKind::File,
logical_size: 5,
created_at_ms: 0,
modified_at_ms: 0,
accessed_at_ms: 0,
revision: Revision::INITIAL,
attributes: BTreeMap::new(),
}
}
const HOSTILE_PATH: &str = "/innocent\x1b[31mFAKE ERROR\x1b[0m.txt";
const HOSTILE_SEGMENT_LABEL: &str = "innocent";
#[test]
fn human_rendering_of_a_node_includes_its_name_and_size() {
let output = CommandOutput::Node(sample_node("a.txt"));
let rendered = render_human(&output);
assert!(rendered.contains("a.txt"));
assert!(rendered.contains('5'));
}
#[test]
fn json_rendering_is_valid_json_matching_the_wire_codec() {
let output = CommandOutput::Node(sample_node("a.txt"));
let rendered = render_json(&output);
let parsed: CommandOutput = serde_json::from_str(&rendered).unwrap();
assert_eq!(parsed, output);
}
#[test]
fn sanitize_strips_the_escape_byte_that_triggers_ansi_sequences() {
let malicious = "innocuous.txt\x1b[31mFAKE ERROR\x1b[0m";
let clean = sanitize_for_terminal(malicious);
assert!(!clean.contains('\x1b'));
assert!(clean.contains("innocuous.txt"));
}
#[test]
fn sanitize_strips_other_control_bytes_but_keeps_newline_and_tab() {
let input = "a\x07b\nc\td";
let clean = sanitize_for_terminal(input);
assert_eq!(clean, "ab\nc\td");
}
#[test]
fn sanitize_name_strips_newline_and_tab_in_addition_to_other_control_bytes() {
let input = "a\x07b\nc\td\x1be";
let clean = sanitize_name(input);
assert_eq!(clean, "abcde");
}
#[test]
fn human_rendering_of_a_node_with_an_embedded_newline_does_not_forge_an_extra_row() {
let hostile_name = "a.txt\nfile 999 IMPORTANT-SYSTEM-FILE.txt";
let output = CommandOutput::Nodes(Page::new(
vec![sample_node("legit.txt"), sample_node(hostile_name)],
None,
));
let rendered = render_human(&output);
let lines: Vec<&str> = rendered.lines().collect();
assert_eq!(
lines.len(),
2,
"rendered output had a forged extra line: {rendered:?}"
);
let matches_naming_the_fake_file = lines
.iter()
.filter(|line| line.contains("IMPORTANT-SYSTEM-FILE"))
.count();
assert_eq!(
matches_naming_the_fake_file, 1,
"the fabricated text should appear on exactly one (merged) line: {rendered:?}"
);
}
#[test]
fn human_rendering_of_a_node_with_a_hostile_name_is_sanitized() {
let hostile_name = "\x1b]0;pwned\x07innocent.txt";
let output = CommandOutput::Node(sample_node(hostile_name));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains("innocent.txt"));
}
#[test]
fn human_rendering_of_nodes_page_sanitizes_every_item() {
let output = CommandOutput::Nodes(Page::new(
vec![sample_node("a.txt"), sample_node("\x1b[2Jinnocent.txt")],
None,
));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains("a.txt"));
assert!(rendered.contains("innocent.txt"));
}
#[test]
fn human_rendering_of_tree_sanitizes_hostile_paths() {
let node = sample_node("ignored");
let entry = TreeEntry {
path: VirtualPath::parse(HOSTILE_PATH).unwrap(),
depth: 1,
node,
};
let output = CommandOutput::Tree(Page::new(vec![entry], None));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains(HOSTILE_SEGMENT_LABEL));
}
#[test]
fn human_rendering_of_link_target_is_sanitized() {
let target = LinkTarget::parse("\x1b[31m/etc/passwd").unwrap();
let output = CommandOutput::LinkTarget(target);
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains("/etc/passwd"));
}
#[test]
fn human_rendering_of_trash_entry_sanitizes_original_path() {
let entry = TrashEntry {
id: TrashId::new(),
node: sample_node("ignored"),
original_path: VirtualPath::parse(HOSTILE_PATH).unwrap(),
trashed_at_ms: 0,
actor_metadata: BTreeMap::new(),
};
let output = CommandOutput::Trash(entry);
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains(HOSTILE_SEGMENT_LABEL));
}
#[test]
fn human_rendering_of_trash_list_sanitizes_every_item() {
let entry = TrashEntry {
id: TrashId::new(),
node: sample_node("ignored"),
original_path: VirtualPath::parse(HOSTILE_PATH).unwrap(),
trashed_at_ms: 0,
actor_metadata: BTreeMap::new(),
};
let output = CommandOutput::TrashList(Page::new(vec![entry], None));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains(HOSTILE_SEGMENT_LABEL));
}
#[test]
fn human_rendering_of_search_matches_sanitizes_path_and_preview() {
let search_match = SearchMatch {
node: sample_node("ignored"),
path: VirtualPath::parse(HOSTILE_PATH).unwrap(),
range: ByteRange::new(0, 5),
preview: b"safe\x1b[31mtext".to_vec(),
};
let output = CommandOutput::SearchMatches(Page::new(vec![search_match], None));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
assert!(rendered.contains(HOSTILE_SEGMENT_LABEL));
assert!(rendered.contains("safe"));
assert!(rendered.contains("text"));
}
#[test]
fn human_rendering_of_changes_contains_no_control_bytes() {
let change = Change {
sequence: 1,
kind: ChangeKind::Moved,
node_id: Some(NodeId::new()),
old_path: Some(VirtualPath::parse(HOSTILE_PATH).unwrap()),
new_path: Some(VirtualPath::parse("/dest.txt").unwrap()),
revision: Some(Revision::INITIAL),
created_at_ms: 0,
actor_metadata: BTreeMap::new(),
};
let output = CommandOutput::Changes(Page::new(vec![change], None));
let rendered = render_human(&output);
assert!(!rendered.contains('\x1b'));
}
#[test]
fn json_rendering_round_trips_every_variant_shape() {
let node = sample_node("a.txt");
let outputs = vec![
CommandOutput::Exists(true),
CommandOutput::Unit,
CommandOutput::Node(node.clone()),
CommandOutput::Nodes(Page::new(vec![node.clone()], None)),
CommandOutput::LinkTarget(LinkTarget::parse("/target").unwrap()),
CommandOutput::Batch(vec![fslite_core::BatchResult::Node(node)]),
];
for output in outputs {
let rendered = render_json(&output);
let parsed: CommandOutput = serde_json::from_str(&rendered).unwrap();
assert_eq!(parsed, output);
}
}
#[test]
fn sanitize_name_is_reachable_from_the_crate_root() {
let clean = fslite_command::sanitize_name("a\nb");
assert_eq!(clean, "ab");
}
#[test]
fn sanitize_preview_is_reachable_from_the_crate_root() {
let escaped = fslite_command::sanitize_preview("a\nb");
assert_eq!(escaped, "a\\nb");
}
#[test]
fn human_rendering_of_search_matches_does_not_forge_an_extra_row_from_a_newline_in_the_preview() {
let search_match = SearchMatch {
node: sample_node("ignored"),
path: VirtualPath::parse("/real.txt").unwrap(),
range: ByteRange::new(0, 5),
preview: b"needle\n/etc/shadow: root:x:0:0".to_vec(),
};
let output = CommandOutput::SearchMatches(Page::new(vec![search_match], None));
let rendered = render_human(&output);
let lines: Vec<&str> = rendered.lines().collect();
assert_eq!(
lines.len(),
1,
"rendered output had a forged extra line: {rendered:?}"
);
assert!(
rendered.contains("needle\\n/etc/shadow"),
"expected the embedded newline to survive as a visible escape sequence, got: {rendered:?}"
);
}
#[test]
fn sanitize_name_strips_bidi_override_characters() {
let hostile_name = "harmless\u{202E}gpj.exe";
let clean = sanitize_name(hostile_name);
assert!(
!clean.contains('\u{202E}'),
"RLO character survived: {clean:?}"
);
assert!(clean.contains("harmless"));
assert_eq!(clean, "harmlessgpj.exe");
}
#[test]
fn sanitize_name_strips_bidi_marks_not_just_overrides() {
let hostile_name = "harmless\u{200E}file.txt";
let clean = sanitize_name(hostile_name);
assert!(
!clean.contains('\u{200E}'),
"LRM character survived: {clean:?}"
);
assert_eq!(clean, "harmlessfile.txt");
}
#[test]
fn sanitize_name_strips_unicode_line_and_paragraph_separators() {
let hostile_name = "a.txt\u{2028}file 999 IMPORTANT.txt";
let clean = sanitize_name(hostile_name);
assert_eq!(clean, "a.txtfile 999 IMPORTANT.txt");
}
#[test]
fn sanitize_for_terminal_strips_bidi_overrides_but_preserves_unicode_linebreaks() {
let input = "safe\u{202E}text\u{2028}more";
let clean = sanitize_for_terminal(input);
assert_eq!(clean, "safetext\u{2028}more");
}
#[test]
fn sanitize_preview_escapes_unicode_line_separator_too() {
let input = "needle\u{2028}more content";
let escaped = sanitize_preview(input);
assert!(!escaped.contains('\u{2028}'));
assert!(escaped.contains("needle\\u{2028}more"));
}