fix(cli): sanitize item titles in commit messages (audit I1)
Control characters (newlines, tabs) in item titles corrupted git log output. Now strips control chars and truncates to 50 chars. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,21 @@ pub fn write_groups_cache(
|
||||
std::fs::write(path, body)
|
||||
}
|
||||
|
||||
/// Sanitize a string for use in a git commit message subject line.
|
||||
///
|
||||
/// Removes all Unicode control characters (U+0000–U+001F, U+007F, and higher
|
||||
/// control planes) so that newlines and escape sequences cannot corrupt `git
|
||||
/// log` output. Truncates to 50 characters so the subject line stays within
|
||||
/// the conventional limit.
|
||||
///
|
||||
/// Audit I1: item titles are user-supplied and may contain arbitrary bytes.
|
||||
pub fn sanitize_for_commit(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| !c.is_control())
|
||||
.take(50)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Decode a QR image at `path`. Returns the otpauth secret (base32) if the
|
||||
/// QR decodes to an `otpauth://...` URI with a `secret` query param.
|
||||
pub fn decode_totp_qr(path: &std::path::Path) -> anyhow::Result<String> {
|
||||
@@ -179,6 +194,29 @@ mod tests {
|
||||
assert_eq!(iso8601(1_776_556_800), "2026-04-19T00:00:00Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_for_commit_strips_control_chars() {
|
||||
assert_eq!(sanitize_for_commit("line1\nline2"), "line1line2");
|
||||
assert_eq!(sanitize_for_commit("a\tb"), "ab");
|
||||
assert_eq!(sanitize_for_commit("normal"), "normal");
|
||||
assert_eq!(sanitize_for_commit("cr\r\nline"), "crline");
|
||||
// ESC (U+001B) is control and gets stripped; bracket sequences are printable
|
||||
assert_eq!(sanitize_for_commit("\x1b[31mred\x1b[0m"), "[31mred[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_for_commit_truncates_to_50() {
|
||||
let long = "a".repeat(60);
|
||||
assert_eq!(sanitize_for_commit(&long).len(), 50);
|
||||
assert_eq!(sanitize_for_commit(&long), "a".repeat(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_for_commit_allows_unicode() {
|
||||
assert_eq!(sanitize_for_commit("cafe\u{0301}"), "cafe\u{0301}");
|
||||
assert_eq!(sanitize_for_commit("emoji \u{1F4AA}"), "emoji \u{1F4AA}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn humanize_age_buckets() {
|
||||
assert_eq!(humanize_age(0), "just now");
|
||||
|
||||
Reference in New Issue
Block a user