From a6ba747329634706a5c938fe73492f558f46ffdd Mon Sep 17 00:00:00 2001 From: MrBeeboh Date: Thu, 22 Oct 2026 20:20:11 -0700 Subject: [PATCH 1/3] dxf: raise the group-code limit to 150 million and allow CADCRAFT_MAX_DXF_TAGS to override it (#1948) --- crates/dxf/src/lib.rs | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index 82ecf55e8..b824e4e57 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -162,16 +162,31 @@ fn typed(code: i32, text: &str) -> Value { } const BINARY_SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0"; -/// Most group codes read from one file. A `Tag` takes 40 bytes plus its text, so this bounds the -/// tag list to a few GB; real drawings stay far below (a 10 MB DWG is typically a few million). -pub const MAX_TAGS: usize = 50_000_000; +/// Most group codes read from one file by default. A `Tag` takes 40 bytes plus its text, so this +/// bounds the tag list to roughly 6 GB plus text. It matches the 1 GB DXF rendition a DWG may +/// convert to (a 1 GB DXF holds well over 50 million group codes, so a lower cap rejected large +/// DWGs that converted fine, issue #1948). Set `CADCRAFT_MAX_DXF_TAGS` to raise or lower it. +pub const MAX_TAGS: usize = 150_000_000; + +/// Environment variable overriding [`MAX_TAGS`] (a positive integer). +pub const MAX_TAGS_ENV: &str = "CADCRAFT_MAX_DXF_TAGS"; + +/// The group-code limit in effect: [`MAX_TAGS_ENV`] when set to a positive integer, else +/// [`MAX_TAGS`]. +pub fn max_tags() -> usize { + max_tags_from(std::env::var(MAX_TAGS_ENV).ok().as_deref()) +} + +fn max_tags_from(v: Option<&str>) -> usize { + v.and_then(|s| s.trim().replace('_', "").parse::().ok()).filter(|&n| n > 0).unwrap_or(MAX_TAGS) +} mod codepage; pub use codepage::decode_code_page; /// Parse a DXF file (ASCII or binary). pub fn parse(bytes: &[u8]) -> Result> { - parse_with_limit(bytes, MAX_TAGS) + parse_with_limit(bytes, max_tags()) } /// [`parse`] with an explicit group-code limit. For ASCII DXF the limit is checked up front from @@ -258,7 +273,7 @@ fn unescape_unicode(s: &str) -> String { } pub fn parse_ascii(text: &str) -> Result> { - parse_ascii_with_limit(text, MAX_TAGS) + parse_ascii_with_limit(text, max_tags()) } #[allow(clippy::while_let_loop)] @@ -344,7 +359,7 @@ impl<'a> Cur<'a> { /// Binary DXF (R13+: 2-byte group codes). pub fn parse_binary(b: &[u8]) -> Result> { - parse_binary_with_limit(b, MAX_TAGS) + parse_binary_with_limit(b, max_tags()) } fn parse_binary_with_limit(b: &[u8], max_tags: usize) -> Result> { @@ -683,3 +698,19 @@ mod tests { assert_eq!(parse(&b).unwrap()[0].str(), "图号"); } } + +#[cfg(test)] +mod limit_tests { + use super::*; + + #[test] + fn tag_limit_defaults_and_env_override() { + assert_eq!(max_tags_from(None), MAX_TAGS); + assert_eq!(max_tags_from(Some("")), MAX_TAGS); + assert_eq!(max_tags_from(Some("0")), MAX_TAGS); + assert_eq!(max_tags_from(Some("abc")), MAX_TAGS); + assert_eq!(max_tags_from(Some(" 400_000_000 ")), 400_000_000); + // Issue #1948: the default must admit a DXF rendition well above 50 million group codes. + assert!(MAX_TAGS >= 100_000_000); + } +} From a13870f376c4e6f6d7e7814ab8238d74424117d5 Mon Sep 17 00:00:00 2001 From: MrBeeboh Date: Thu, 22 Oct 2026 20:56:55 -0700 Subject: [PATCH 2/3] dxf: check the tag limit in a const block --- crates/dxf/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index b824e4e57..4eacd5cfc 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -711,6 +711,6 @@ mod limit_tests { assert_eq!(max_tags_from(Some("abc")), MAX_TAGS); assert_eq!(max_tags_from(Some(" 400_000_000 ")), 400_000_000); // Issue #1948: the default must admit a DXF rendition well above 50 million group codes. - assert!(MAX_TAGS >= 100_000_000); + const { assert!(MAX_TAGS >= 100_000_000) }; } } From e24c4553d2ca715073383720c9e2d7aa0e98f8dd Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Mon, 2 Nov 2026 19:47:36 -0700 Subject: [PATCH 3/3] DXF: 100 million group codes by default, and memory for them that can be refused The tag list for a counted ASCII file is reserved once with a fallible allocation, so where the system refuses the memory, opening fails with an error instead of ending the app. Growing it would also take half as much again for a moment. 100 million rather than 150 bounds a crafted 512 MB file to about 4 GB of tags; CADCRAFT_MAX_DXF_TAGS raises it, and the error says so. --- crates/dxf/src/lib.rs | 33 +++++++++++++++++++++++++-------- crates/io/src/tests.rs | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index ab2597a21..c31defc21 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -23,8 +23,11 @@ pub enum DxfError { Eof, /// More group codes than [`MAX_TAGS`]: `tags` is the count found (or the count read when the /// limit was reached), `limit` the maximum. - #[error("drawing too large: {tags} DXF group codes, more than the {limit} CADCraft reads")] + #[error("drawing too large: {tags} DXF group codes, more than the {limit} CADCraft reads (set CADCRAFT_MAX_DXF_TAGS to read more)")] TooLarge { tags: usize, limit: usize }, + /// The memory for `tags` group codes couldn't be had: an error, not the process ending. + #[error("not enough memory to read this drawing's {tags} DXF group codes")] + OutOfMemory { tags: usize }, } pub type Result = std::result::Result; @@ -164,10 +167,11 @@ fn typed(code: i32, text: &str) -> Value { const BINARY_SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0"; /// Most group codes read from one file by default. A `Tag` takes 40 bytes plus its text, so this -/// bounds the tag list to roughly 6 GB plus text. It matches the 1 GB DXF rendition a DWG may -/// convert to (a 1 GB DXF holds well over 50 million group codes, so a lower cap rejected large -/// DWGs that converted fine, issue #1948). Set `CADCRAFT_MAX_DXF_TAGS` to raise or lower it. -pub const MAX_TAGS: usize = 150_000_000; +/// bounds the tag list to about 4 GB plus text. 50 million rejected large DWGs that converted fine +/// (a 172 MB drawing has 56 million, issue #1948). The file-size limits bound memory too: 512 MB +/// for a DXF opened directly, 1 GB for a DWG's DXF rendition. Set `CADCRAFT_MAX_DXF_TAGS` to +/// raise or lower it. +pub const MAX_TAGS: usize = 100_000_000; /// Environment variable overriding [`MAX_TAGS`] (a positive integer). pub const MAX_TAGS_ENV: &str = "CADCRAFT_MAX_DXF_TAGS"; @@ -214,7 +218,7 @@ pub fn parse_with_limit(bytes: &[u8], max_tags: usize) -> Result> { if tags > max_tags { return Err(DxfError::TooLarge { tags, limit: max_tags }); } - parse_ascii_with_limit(&codepage::decode_ascii_file(bytes), max_tags) + parse_ascii_counted(&codepage::decode_ascii_file(bytes), max_tags, tags) } /// The four hex digits of a `\U+XXXX` escape starting at `chars[i]`, if there is one. @@ -286,12 +290,17 @@ fn unescape_unicode(s: &str) -> String { } pub fn parse_ascii(text: &str) -> Result> { - parse_ascii_with_limit(text, max_tags()) + parse_ascii_counted(text, max_tags(), 0) } +/// [`parse_ascii`] of a file known to hold `expected` group codes. Their list is reserved first, +/// with an allocation that can fail: where the system refuses that much memory, opening the file +/// is an error instead of the end of the app. Growing the list instead could also briefly take +/// half as much again. #[allow(clippy::while_let_loop)] -fn parse_ascii_with_limit(text: &str, max_tags: usize) -> Result> { +fn parse_ascii_counted(text: &str, max_tags: usize, expected: usize) -> Result> { let mut tags = Vec::new(); + tags.try_reserve_exact(expected.min(max_tags)).map_err(|_| DxfError::OutOfMemory { tags: expected })?; let mut lines = text.strip_prefix('\u{feff}').unwrap_or(text).lines().enumerate(); loop { let Some((ln, code_line)) = lines.next() else { break }; @@ -807,4 +816,12 @@ mod limit_tests { // Issue #1948: the default must admit a DXF rendition well above 50 million group codes. const { assert!(MAX_TAGS >= 100_000_000) }; } + + #[test] + fn the_tag_list_is_reserved_once_and_a_refusal_is_an_error() { + let tags = parse_ascii_counted("0\nSECTION\n0\nENDSEC\n0\nEOF\n", MAX_TAGS, 3).unwrap(); + assert_eq!((tags.len(), tags.capacity()), (3, 3)); + let e = parse_ascii_counted("0\nEOF\n", usize::MAX, usize::MAX).unwrap_err(); + assert!(matches!(e, DxfError::OutOfMemory { .. }), "{e}"); + } } diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index 5d08817c5..fca680385 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -1039,7 +1039,7 @@ fn oversized_dwg_and_dxf_are_rejected_early() { // Lines that aren't even group codes: the size is reported before anything is parsed. let text = "not a group code\nx\n".repeat(60); let e = cadcraft_dxf::parse_with_limit(text.as_bytes(), 50).unwrap_err(); - assert_eq!(e.to_string(), "drawing too large: 60 DXF group codes, more than the 50 CADCraft reads"); + assert_eq!(e.to_string(), "drawing too large: 60 DXF group codes, more than the 50 CADCraft reads (set CADCRAFT_MAX_DXF_TAGS to read more)"); } /// Issue #800: one unreadable object record doesn't fail the whole DWG. The reader skips it, the