fix(read): prioritize known text extensions - #3848
original4422 wants to merge 2 commits into
Conversation
Co-Authored-By: ForgeCode <noreply@forgecode.dev>
|
Hi @tusharmath, when you have a chance, would you mind reviewing this MIME-detection fix and letting me know if any changes are needed? Thank you! Co-Authored-By: ForgeCode noreply@forgecode.dev |
|
Action required: PR inactive for 5 days. |
|
Status update: this PR is still active and ready for review. The current head remains Validation recorded for this unchanged head includes 28/28 focused Maintainers, could you please review this when available and approve the gated workflows if appropriate? Thank you. Co-Authored-By: ForgeCode noreply@forgecode.dev |
|
Action required: PR inactive for 5 days. |
|
Status update: this PR is still active and ready for review at the unchanged head Co-Authored-By: ForgeCode noreply@forgecode.dev |
|
Action required: PR inactive for 5 days. |
|
Review of the unchanged head Independent verification on Linux:
The additions exercise One root-cause clarification: infer 0.22's PDF matcher already searches only the first 1024 bytes, rather than the entire buffer. The reported offset 449 is within that window, so the suggested 1 KiB cap cannot fix the reproduction. Unknown/extensionless files intentionally retain sniffing, including embedded PDF magic. Related #3633 / #3749 changes Remaining verification blockers (not ready-to-merge evidence):
@original4422, please consider folding in the optional test-only patch below, or equivalent coverage. It applies on your current head and preserves all your existing code/tests. This comment shares the additions without pushing your branch or duplicating your PR. The existing non-draft PR has been left as-is. Optional supplemental regression patch (test code only)diff --git a/crates/forge_services/src/attachment.rs b/crates/forge_services/src/attachment.rs
index 7b3da13d7..04ea8d208 100644
--- a/crates/forge_services/src/attachment.rs
+++ b/crates/forge_services/src/attachment.rs
@@ -475,6 +475,15 @@ pub mod tests {
pub fn add_file(&self, path: PathBuf, content: String) {
self.file_service.add_file(path, content);
}
+
+ /// Adds raw bytes without requiring binary fixtures to be valid UTF-8.
+ pub fn add_bytes(&self, path: PathBuf, content: Vec<u8>) {
+ self.file_service
+ .files
+ .lock()
+ .unwrap()
+ .push((path, Bytes::from(content)));
+ }
}
#[async_trait::async_trait]
diff --git a/crates/forge_services/src/tool_services/fs_read.rs b/crates/forge_services/src/tool_services/fs_read.rs
index 6a71d0fd8..0f379b9c9 100644
--- a/crates/forge_services/src/tool_services/fs_read.rs
+++ b/crates/forge_services/src/tool_services/fs_read.rs
@@ -394,6 +394,136 @@ mod tests {
assert_eq!(actual, expected);
}
+ fn read_fixture(path: &str, content: &[u8]) -> ForgeFsRead<MockCompositeService> {
+ let infra = Arc::new(MockCompositeService::new());
+ infra.add_bytes(PathBuf::from(path), content.to_vec());
+ ForgeFsRead::new(infra)
+ }
+
+ fn png_fixture() -> anyhow::Result<Vec<u8>> {
+ use base64::Engine;
+
+ // Complete one-pixel RGB PNG, including IHDR, IDAT and IEND chunks.
+ base64::engine::general_purpose::STANDARD
+ .decode(concat!(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1Pe",
+ "AAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
+ ))
+ .context("Failed to decode PNG fixture")
+ }
+
+ #[tokio::test]
+ async fn test_fs_read_magic_source_matrix() {
+ let sources = [
+ typescript_with_embedded_pdf_magic(),
+ r#"const magic = "\x89PNG";"#.to_string(),
+ "// PNG signature: ‰PNG".to_string(),
+ "GIF89a; // A source identifier that also matches image magic".to_string(),
+ ];
+ let extensions = [
+ "txt", "md", "rs", "toml", "yaml", "yml", "json", "js", "ts", "py", "sh", "ipynb",
+ ];
+ for source in sources {
+ for extension in extensions {
+ for extension in [extension.to_string(), extension.to_ascii_uppercase()] {
+ let path = format!("/test/source.{extension}");
+ let fixture = read_fixture(&path, source.as_bytes());
+
+ let output = fixture.read(path, None, None).await.unwrap();
+ let actual = (output.content.file_content().to_string(), output.info);
+
+ let expected = (
+ source.clone(),
+ FileInfo::new(1, 2000, 1, compute_hash(&source)),
+ );
+ assert_eq!(actual, expected);
+ assert!(output.content.as_image().is_none());
+ }
+ }
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fs_read_binary_png_preserves_visual_payload() {
+ let content = png_fixture().unwrap();
+ for path in [
+ "/test/pixel.png",
+ "/test/pixel.PNG",
+ "/test/pixel.unknown",
+ "/test/pixel",
+ "/test/pixel.pdf",
+ ] {
+ let fixture = read_fixture(path, &content);
+
+ let output = fixture.read(path.to_string(), None, None).await.unwrap();
+ let actual = (output.content.as_image().cloned(), output.info);
+
+ let image = Image::new_bytes(content.clone(), "image/png");
+ let info = FileInfo::new(0, 0, 0, compute_hash(image.url()));
+ let expected = (Some(image), info);
+ assert_eq!(actual, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fs_read_invalid_utf8_with_text_extension_is_not_visual() {
+ let content = png_fixture().unwrap();
+ let path = "/test/not_source.ts";
+ let fixture = read_fixture(path, &content);
+
+ let actual = fixture
+ .read(path.to_string(), None, None)
+ .await
+ .unwrap_err();
+
+ let expected = format!("Failed to read file as UTF-8 from {path}");
+ assert_eq!(actual.to_string(), expected);
+ }
+
+ #[tokio::test]
+ async fn test_fs_read_unknown_and_extensionless_text_fallback() {
+ for path in ["/test/source.unknown", "/test/source"] {
+ for content in ["plain text", "", r#"const magic = "\x89PNG";"#] {
+ let fixture = read_fixture(path, content.as_bytes());
+
+ let output = fixture.read(path.to_string(), None, None).await.unwrap();
+ let actual = (output.content.file_content().to_string(), output.info);
+
+ let expected = (
+ content.to_string(),
+ FileInfo::new(
+ 1,
+ 2000,
+ u64::from(!content.is_empty()),
+ compute_hash(content),
+ ),
+ );
+ assert_eq!(actual, expected);
+ assert!(output.content.as_image().is_none());
+ }
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fs_read_unknown_and_extensionless_pdf_magic_remains_visual() {
+ let content = typescript_with_embedded_pdf_magic();
+ for path in [
+ "/test/document.pdf",
+ "/test/document.unknown",
+ "/test/document",
+ ] {
+ let fixture = read_fixture(path, content.as_bytes());
+
+ let output = fixture.read(path.to_string(), None, None).await.unwrap();
+ let actual = (output.content.as_image().cloned(), output.info);
+
+ let image = Image::new_bytes(content.as_bytes().to_vec(), "application/pdf");
+ let info = FileInfo::new(0, 0, 0, compute_hash(image.url()));
+ let expected = (Some(image), info);
+ assert_eq!(actual, expected);
+ }
+ }
+
#[test]
fn test_detect_mime_type_for_ipynb() {
let fixture = typescript_with_embedded_pdf_magic();Co-Authored-By: ForgeCode noreply@forgecode.dev |
Co-Authored-By: ForgeCode <noreply@forgecode.dev>
|
Thanks for the independent verification and supplemental patch. I folded in a focused subset as commit 600e237: the real ForgeFsRead path now verifies that .pdf, unknown-extension, and extensionless inputs with embedded PDF magic remain visual content. I left out the broader raw-byte helper and 96-case matrix because the existing tests already cover the full known-text allowlist and the reported source-file path, so this keeps the PR regression-focused. Validation on the submitted head:
Co-Authored-By: ForgeCode noreply@forgecode.dev |
Summary
inferdetection for unknown and extensionless files and preserve the existing extension fallback.ForgeFsReadpath and MIME compatibility matrix with same-file regression tests.Root cause
infer::getscans the entire supplied buffer and recognizes the%PDFsequence at byte offset 449. Limiting the buffer to the issue's suggested 1 KiB would still include that sequence and therefore would not fix this reproduction. The minimal reliable fix is to trust the existing known-text extension allowlist before content sniffing.Validation
cargo test -p forge_services tool_services::fs_read::tests -- --nocapture— 28 passedcargo test -p forge_services— 216 passed; doc tests passedcargo check -p forge_services— passedcargo clippy -p forge_services --all-targets --all-features -- -D warnings— passedcargo fmt -p forge_services -- --check— passedgit diff --check— passedcargo insta testwas unavailable because the localcargo-instacommand is not installed, so the complete crate suite was run directly withcargo test. A workspace-wide clippy attempt stopped while building the untouchedforge_repocrate because localprotocis absent; CI installsprotoc, and the touched crate's all-target/all-feature clippy is green.Fixes #3812
Co-Authored-By: ForgeCode noreply@forgecode.dev