From 936f08db1feba5515f5ea7d51a9d3a37dda3aecd Mon Sep 17 00:00:00 2001 From: Rodrigo Gressler Date: Mon, 19 Oct 2026 22:25:35 -0300 Subject: [PATCH] DXF: keep files that declare ASCII or UTF8 pure ASCII when saving A pre-2007 file whose header says $DWGCODEPAGE ASCII (or UTF8/UNICODE, which no pre-2007 reader expects) is still written with \U+ escapes only, instead of Windows-1252 (or UTF-8) bytes the header doesn't declare. Review of #1693. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/dxf/src/codepage.rs | 14 +++++++++++++- crates/dxf/src/lib.rs | 7 ++++--- crates/dxf/tests/code_page_write.rs | 18 +++++++++++------- crates/io/tests/codepages.rs | 19 +++++++++++++++++++ 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/crates/dxf/src/codepage.rs b/crates/dxf/src/codepage.rs index 53f24c3b6..9377404d9 100644 --- a/crates/dxf/src/codepage.rs +++ b/crates/dxf/src/codepage.rs @@ -69,12 +69,24 @@ pub fn decode_code_page(bytes: &[u8], code_page: &str) -> Option { decoder(code_page).map(|d| decode(bytes, d).into_owned()) } +/// The code page a pre-2007 file's text is written in, or `None` for pure ASCII with escapes: +/// when CADCraft doesn't know `code_page`, and for `ASCII` (the reader takes such files as +/// Windows-1252, but a writer must keep them ASCII) and `UTF8`/`UNICODE`, which no pre-2007 +/// reader expects. +pub(crate) fn encoder(code_page: &str) -> Option { + if code_page.trim().eq_ignore_ascii_case("ASCII") { + return None; + } + decoder(code_page).filter(|d| !matches!(d, Decoder::Utf8)) +} + /// Append the bytes of the non-ASCII character `c` in the code page `d`, and return true, when /// the code page has it; otherwise append nothing and return false (the caller escapes it). pub(crate) fn encode_char(c: char, d: Decoder, out: &mut Vec) -> bool { let mut buf = [0; 4]; match d { - Decoder::Utf8 => out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes()), + // Not a pre-2007 code page ([`encoder`] never gives it): escaped. + Decoder::Utf8 => return false, Decoder::Encoding(e) => { let (bytes, _, unmappable) = e.encode(c.encode_utf8(&mut buf)); // A code page byte that is ASCII would read back as another character, and a NUL, diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index e37e3e13e..0bbb4ee68 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -418,9 +418,10 @@ pub fn write_ascii_as(tags: &[Tag], utf8: bool) -> String { /// Write tags as pre-R2007 ASCII DXF with text in the code page `code_page` names (a /// `$DWGCODEPAGE` value such as `ANSI_1252`), as the DXF Reference describes for those versions: /// a character the code page has is written as its bytes, any other as a `\U+XXXX` escape. A -/// code page CADCraft doesn't know gives [`write_ascii`]'s pure ASCII output. +/// code page CADCraft doesn't know, `ASCII`, and `UTF8`/`UNICODE` (not a pre-2007 code page) +/// give [`write_ascii`]'s pure ASCII output. pub fn write_ascii_in(tags: &[Tag], code_page: &str) -> Vec { - ascii(tags, Strings::Escaped(codepage::decoder(code_page))) + ascii(tags, Strings::Escaped(codepage::encoder(code_page))) } /// How a writer stores string values. @@ -463,7 +464,7 @@ pub fn write_binary(tags: &[Tag], utf8: bool) -> Vec { /// Write tags as pre-R2007 binary DXF with text in the code page `code_page` names, as /// [`write_ascii_in`] does. pub fn write_binary_in(tags: &[Tag], code_page: &str) -> Vec { - binary(tags, Strings::Escaped(codepage::decoder(code_page))) + binary(tags, Strings::Escaped(codepage::encoder(code_page))) } fn binary(tags: &[Tag], strings: Strings) -> Vec { diff --git a/crates/dxf/tests/code_page_write.rs b/crates/dxf/tests/code_page_write.rs index 316f2edd5..67e7e7e57 100644 --- a/crates/dxf/tests/code_page_write.rs +++ b/crates/dxf/tests/code_page_write.rs @@ -46,12 +46,16 @@ fn text_is_written_in_the_code_page() { } } -/// Without a known code page the output is what `write_ascii` writes, line breaks included. +/// Without a known code page, and for `ASCII`, `UTF8` and `UNICODE` headers, the output is what +/// `write_ascii` writes (pure ASCII with escapes), line breaks included. #[test] -fn unknown_code_page_is_pure_ascii() { - let tags = file("EBCDIC", "Ação\r\nfim"); - let bytes = write_ascii_in(&tags, "EBCDIC"); - assert!(bytes.is_ascii()); - assert_eq!(bytes, write_ascii(&tags).into_bytes()); - assert!(contains(&bytes, b"A\\U+00E7\\U+00E3o fim")); +fn unknown_ascii_and_utf8_code_pages_are_pure_ascii() { + for cp in ["EBCDIC", "ASCII", "ascii ", "UTF8", "UNICODE"] { + let tags = file(cp, "Ação\r\nfim"); + let bytes = write_ascii_in(&tags, cp); + assert!(bytes.is_ascii(), "{cp}"); + assert_eq!(bytes, write_ascii(&tags).into_bytes(), "{cp}"); + assert!(contains(&bytes, b"A\\U+00E7\\U+00E3o fim"), "{cp}"); + assert!(write_binary_in(&tags, cp).iter().all(u8::is_ascii), "{cp} binary"); + } } diff --git a/crates/io/tests/codepages.rs b/crates/io/tests/codepages.rs index 53c22e908..56f9af80f 100644 --- a/crates/io/tests/codepages.rs +++ b/crates/io/tests/codepages.rs @@ -146,6 +146,25 @@ fn saving_writes_text_in_the_code_page() { } } +/// A file whose header declares `ASCII` keeps that code page and stays ASCII when saved: its +/// non-ASCII text is written as `\U+` escapes, not as Windows-1252 bytes. +#[test] +fn saving_an_ascii_file_keeps_it_ascii() { + use cadcraft_io::{SaveOptions, Version}; + let d = cadcraft_io::read_dxf(&ascii_dxf("AC1015", "ASCII", b"El\\U+00E9trica", b"B", b"Eletrica \\U+00E9")).unwrap(); + assert_eq!(names(&d), ("Elétrica".into(), "B".into(), "Eletrica é".into())); + for version in [Version::R2000, Version::R2004] { + let ascii = cadcraft_io::write_with(&d, "x.dxf", &SaveOptions { version: Some(version), binary: false }).unwrap(); + assert!(ascii.is_ascii(), "{version}"); + let binary = cadcraft_io::write_with(&d, "x.dxf", &SaveOptions { version: Some(version), binary: true }).unwrap(); + for out in [&ascii, &binary] { + assert!(out.windows(14).any(|w| w == b"El\\U+00E9trica"), "{version}"); + assert!(out.windows(5).any(|w| w == b"ASCII"), "{version}: the header keeps its code page"); + assert_eq!(names(&cadcraft_io::read_dxf(out).unwrap()), ("Elétrica".into(), "B".into(), "Eletrica é".into()), "{version}"); + } + } +} + /// A binary DXF: R2000 strings are in the code page, R2007+ strings UTF-8. #[test] fn binary_dxf_honours_the_code_page() {