fix/audit-findings #5
2 changed files with 51 additions and 2 deletions
fix(scan): decode non-UTF-8 source lossily instead of dropping the file
A single invalid byte, such as a Latin-1 `é` in a comment, made `read_to_string` fail and the whole file and its symbols were skipped. Files are now read as bytes: a NUL byte still marks a binary file, which is skipped with a diagnostic; otherwise invalid UTF-8 is decoded lossily (U+FFFD) and a warning diagnostic records it, so the file keeps its symbols and calls. Test: languages::a_non_utf8_byte_does_not_drop_the_whole_file fails before this change (latin1.rs has no symbols).
commit
9f9fc560b7
26
src/scan.rs
26
src/scan.rs
|
|
@ -368,17 +368,39 @@ impl ScanState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let content = match fs::read_to_string(path) {
|
let bytes = match fs::read(path) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.diagnostics.push(Diagnostic {
|
self.diagnostics.push(Diagnostic {
|
||||||
level: "warning".into(),
|
level: "warning".into(),
|
||||||
message: format!("skipped non-UTF-8 or unreadable source: {error}"),
|
message: format!("skipped unreadable source: {error}"),
|
||||||
path: Some(relative),
|
path: Some(relative),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// NUL bytes mean a binary file, not source. Any other invalid UTF-8 (a
|
||||||
|
// Latin-1 byte in a comment) is decoded lossily: dropping the whole file hid
|
||||||
|
// every symbol in it.
|
||||||
|
if bytes.contains(&0) {
|
||||||
|
self.diagnostics.push(Diagnostic {
|
||||||
|
level: "warning".into(),
|
||||||
|
message: "skipped binary file with a source extension".into(),
|
||||||
|
path: Some(relative),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let content = match String::from_utf8(bytes) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
self.diagnostics.push(Diagnostic {
|
||||||
|
level: "warning".into(),
|
||||||
|
message: "decoded non-UTF-8 bytes lossily (U+FFFD)".into(),
|
||||||
|
path: Some(relative.clone()),
|
||||||
|
});
|
||||||
|
String::from_utf8_lossy(error.as_bytes()).into_owned()
|
||||||
|
}
|
||||||
|
};
|
||||||
let file_id = stable_id("file", &relative);
|
let file_id = stable_id("file", &relative);
|
||||||
let parent = path
|
let parent = path
|
||||||
.parent()
|
.parent()
|
||||||
|
|
|
||||||
|
|
@ -702,3 +702,30 @@ fn dart_calls_resolve_through_imported_libraries() {
|
||||||
[("helper".into(), String::new(), "not-visible".into())]
|
[("helper".into(), String::new(), "not-visible".into())]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_non_utf8_byte_does_not_drop_the_whole_file() {
|
||||||
|
// One Latin-1 `é` in a comment made the whole file (and its functions) vanish.
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut source = b"// caf".to_vec();
|
||||||
|
source.push(0xE9);
|
||||||
|
source.extend_from_slice(b"\nfn kept() {}\nfn caller() { kept(); }\n");
|
||||||
|
fs::write(dir.path().join("latin1.rs"), source).unwrap();
|
||||||
|
fs::write(dir.path().join("blob.rs"), [0u8, 159, 146, 150]).unwrap();
|
||||||
|
let model = scan::scan(dir.path()).unwrap();
|
||||||
|
let caller = symbol(&model, "latin1.rs", "caller");
|
||||||
|
assert!(calls(&model, caller, symbol(&model, "latin1.rs", "kept")));
|
||||||
|
assert!(
|
||||||
|
model
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|d| d.path.as_deref() == Some("latin1.rs") && d.message.contains("non-UTF-8"))
|
||||||
|
);
|
||||||
|
// A file with NUL bytes is binary, not source: still skipped with a diagnostic.
|
||||||
|
assert!(
|
||||||
|
!model
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.path.as_deref() == Some("blob.rs"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue