fix: Properly escape JSON string output in CLI binaries

The hand-rolled JSON escaping only handled \, ", and \n but missed
tabs, carriage returns, and other control characters (U+0000..U+001F),
producing invalid JSON that Python's json.loads would reject. Add a
proper json_escape() function covering all required escapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-19 10:52:31 -08:00
co-authored by Claude Opus 4.6
parent 7a5af1e9c3
commit 23056dc5ba
2 changed files with 49 additions and 6 deletions
+23 -1
View File
@@ -5,9 +5,31 @@ use pdf_inspector::{
ProcessMode,
};
use std::env;
use std::fmt::Write;
use std::process;
use std::time::Instant;
/// Escape a string for embedding in a JSON string value.
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 16);
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
c if c < '\x20' => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
fn main() {
env_logger::init();
let args: Vec<String> = env::args().collect();
@@ -152,7 +174,7 @@ fn run_detect_only(pdf_path: &str, json_output: bool, start: Instant) {
result
.title
.as_ref()
.map(|t| format!("\"{}\"", t.replace('"', "\\\"")))
.map(|t| format!("\"{}\"", json_escape(t)))
.unwrap_or_else(|| "null".to_string()),
result.ocr_recommended,
ocr_pages.join(","),
+26 -5
View File
@@ -6,9 +6,34 @@ use pdf_inspector::{
};
use std::collections::HashSet;
use std::env;
use std::fmt::Write;
use std::fs;
use std::process;
/// Escape a string for embedding in a JSON string value.
///
/// Handles all characters that the JSON spec requires to be escaped:
/// backslash, double-quote, and control characters U+0000..U+001F.
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 16);
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
c if c < '\x20' => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
/// Parse a page specification like "1,3,5-10,20" into a HashSet of page numbers.
fn parse_page_spec(spec: &str) -> Result<HashSet<u32>, String> {
let mut pages = HashSet::new();
@@ -185,11 +210,7 @@ fn main() {
let md_escaped = result
.markdown
.as_ref()
.map(|m| {
m.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
})
.map(|m| json_escape(m))
.unwrap_or_default();
let ocr_pages: Vec<String> = result