From 0cae1c518b863ab660272b375710a3261e1a3c34 Mon Sep 17 00:00:00 2001 From: oracola91 <118899664+oracola91@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:10:15 +0200 Subject: [PATCH] A language tag longer than 35 characters is still a tag BCP 47 sets no maximum length; 35 characters is only the least a protocol must hold. A well formed tag such as en-US-u-ca-gregory-nu-latn-co-phonebk was left out of both exports. The bound is now 255, kept only so a PDF's /Lang can't put an unbounded string in the file. --- crates/export/src/as_html.rs | 8 ++++++-- crates/export/src/tests.rs | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/export/src/as_html.rs b/crates/export/src/as_html.rs index 6ae767d09c..45f6ecafca 100644 --- a/crates/export/src/as_html.rs +++ b/crates/export/src/as_html.rs @@ -8,14 +8,18 @@ pub(crate) fn allowed(c: char) -> bool { matches!(c, '\t' | '\n' | '\r') || !(c.is_ascii_control() || c == '\u{FFFE}' || c == '\u{FFFF}') } +/// The longest language tag the exports write. BCP 47 sets no maximum (35 characters is only +/// the least a protocol must hold), so this bounds only what a PDF's `/Lang` can put in a file. +const LONGEST_TAG: usize = 255; + /// `lang` when it is a well formed language tag (BCP 47, as a PDF's `/Lang` holds one): a primary /// language of two or three letters, then subtags of one to eight letters or digits, joined by -/// hyphens, 35 characters at most. Anything else names no language. +/// hyphens, [`LONGEST_TAG`] characters at most. Anything else names no language. pub(crate) fn language_tag(lang: Option<&str>) -> Option<&str> { let tag = lang?.trim(); let mut subtags = tag.split('-'); let primary = subtags.next()?; - let well_formed = tag.len() <= 35 + let well_formed = tag.len() <= LONGEST_TAG && (2..=3).contains(&primary.len()) && primary.bytes().all(|b| b.is_ascii_alphabetic()) && subtags.all(|s| (1..=8).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_alphanumeric())); diff --git a/crates/export/src/tests.rs b/crates/export/src/tests.rs index d62148aaf2..ba5f95f66d 100644 --- a/crates/export/src/tests.rs +++ b/crates/export/src/tests.rs @@ -260,6 +260,11 @@ fn word_and_html_take_the_documents_language() { assert!(styles(Some("ar-EG")).contains(&defaults("")), "right-to-left text"); assert!(styles(Some("HE")).contains(""), "right-to-left text"); assert!(html_with_language(&pages, "Report", Some("bg-BG")).starts_with("\n\n")); + // A tag longer than 35 characters is still a tag. + let long = "en-US-u-ca-gregory-nu-latn-co-phonebk"; + assert!(styles(Some(long)).contains(&format!("")), "{long}"); + assert!(html_with_language(&pages, "Report", Some(long)).contains(&format!("")), "{long}"); + let too_long = format!("en{}", "-abcdefgh".repeat(29)); for lang in [ None, Some(""), @@ -270,7 +275,7 @@ fn word_and_html_take_the_documents_language() { Some("bg--BG"), Some("bg-"), Some("en-US-abcdefghi"), - Some("en-abcdefgh-abcdefgh-abcdefgh-abcdefgh"), + Some(too_long.as_str()), Some("en-\"x\""), Some("1a-BG"), Some("\u{431}\u{433}-BG"),