From 043073808f6ed7bd59e83077bb4dc3eb97a73602 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Tue, 13 Oct 2026 21:14:29 +0200 Subject: [PATCH] fix(dxf): hostile-input hardening in the DXF reader and writer (#1025) Binary reals are finite-filtered like ASCII ones; ASCII reads a UTF-8 BOM and CR-only/mixed line ends (and the tag-count precheck counts the same lines); text is decoded per line/string, valid UTF-8 first, else the code page, in both readers; trailing spaces no longer hide SECTION/ENDSEC/EOF; integer groups reject inf/NaN and clamp to their width; hex groups pad an odd digit and drop non-hex characters in reader and writers; the ASCII writer sanitises hex and splits it at 254 digits; write_binary wraps in-range unsigned 16/32-bit values. Co-Authored-By: Claude Sonnet 5.5 --- crates/dxf/src/codepage.rs | 37 ++++++-- crates/dxf/src/lib.rs | 107 ++++++++++++++++++---- crates/dxf/tests/hardening.rs | 167 ++++++++++++++++++++++++++++++++++ crates/io/tests/codepages.rs | 6 +- 4 files changed, 286 insertions(+), 31 deletions(-) create mode 100644 crates/dxf/tests/hardening.rs diff --git a/crates/dxf/src/codepage.rs b/crates/dxf/src/codepage.rs index 6f27b5be4..ddc4ca24f 100644 --- a/crates/dxf/src/codepage.rs +++ b/crates/dxf/src/codepage.rs @@ -93,7 +93,7 @@ pub(crate) fn decode(bytes: &[u8], d: Decoder) -> Cow<'_, str> { /// line (the group code `3` between them), looked for in the HEADER section only (the scan stops /// at the first `ENDSEC`). Works on the raw bytes, before they are decoded. fn ascii_code_page(bytes: &[u8]) -> Option<&[u8]> { - let mut lines = bytes.split(|&b| b == b'\n').map(<[u8]>::trim_ascii); + let mut lines = bytes.split(|&b| b == b'\n' || b == b'\r').map(<[u8]>::trim_ascii).filter(|l| !l.is_empty()); while let Some(line) = lines.next() { if line == b"ENDSEC" { return None; @@ -108,10 +108,21 @@ fn ascii_code_page(bytes: &[u8]) -> Option<&[u8]> { None } -/// Decode a whole ASCII DXF file. Valid UTF-8 is read as UTF-8: R2007+ files, pure-ASCII files, -/// and older files from writers that ignored the code page. Anything else is decoded from the -/// file's `$DWGCODEPAGE`, or from Windows-1252 (the usual `ANSI_1252`) when the header has none -/// or names a code page CADCraft doesn't know, which is logged as a warning. +/// Decode one string of a DXF file: valid UTF-8 is read as UTF-8 (R2007+ files, pure-ASCII text, +/// and older files from writers that ignored the code page); anything else comes from the +/// code page `d`. The same policy for ASCII lines and binary strings, so one stray byte spoils +/// one string, not the whole file. +pub(crate) fn decode_text(bytes: &[u8], d: Decoder) -> Cow<'_, str> { + match std::str::from_utf8(bytes) { + Ok(s) => Cow::Borrowed(s), + Err(_) => decode(bytes, d), + } +} + +/// Decode a whole ASCII DXF file, line by line (see [`decode_text`]). Lines that aren't valid +/// UTF-8 are decoded from the file's `$DWGCODEPAGE`, or from Windows-1252 (the usual `ANSI_1252`) +/// when the header has none or names a code page CADCraft doesn't know, which is logged as a +/// warning. pub(crate) fn decode_ascii_file(bytes: &[u8]) -> Cow<'_, str> { if let Ok(s) = std::str::from_utf8(bytes) { return Cow::Borrowed(s); @@ -126,7 +137,13 @@ pub(crate) fn decode_ascii_file(bytes: &[u8]) -> Cow<'_, str> { } None => Decoder::Utf8, }; - decode(bytes, d) + // Split after each line terminator (CR, LF or CRLF are all ASCII, so they never occur inside + // a multi-byte character of the supported code pages) and decode each line on its own. + let mut out = String::with_capacity(bytes.len()); + for line in bytes.split_inclusive(|&b| b == b'\n' || b == b'\r') { + out.push_str(&decode_text(line, d)); + } + Cow::Owned(out) } /// The text state of a binary DXF file, learnt from its header as the tags go by: `$ACADVER` @@ -147,12 +164,12 @@ enum HeaderVar { } impl BinaryText { - /// Decode one string value of group `code` and update the state from it. Strings of R2007+ - /// files, and of files without a code page, are UTF-8 when valid (else Windows-1252); older - /// files' strings are in their code page. + /// Decode one string value of group `code` and update the state from it. A string that is + /// valid UTF-8 is read as such; otherwise it is in the file's code page (Windows-1252 for + /// R2007+ files and files without a known code page). pub(crate) fn string(&mut self, code: i32, raw: &[u8]) -> String { let d = if self.utf8 { Decoder::Utf8 } else { self.code_page.unwrap_or(Decoder::Utf8) }; - let s = decode(raw, d).into_owned(); + let s = decode_text(raw, d).into_owned(); match (code, self.var.take()) { (9, _) => { self.var = match s.trim() { diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index 82ecf55e8..77b0ad89d 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -61,7 +61,9 @@ impl Value { match self { Value::Int(i) => *i, Value::Real(r) if r.is_finite() => *r as i64, - Value::Str(s) => s.trim().parse::().unwrap_or_else(|_| s.trim().parse::().map(|f| f as i64).unwrap_or(0)), + Value::Str(s) => { + s.trim().parse::().unwrap_or_else(|_| s.trim().parse::().ok().filter(|f| f.is_finite()).map_or(0, |f| f as i64)) + } Value::Bool(b) => i64::from(*b), _ => 0, } @@ -153,15 +155,25 @@ fn typed(code: i32, text: &str) -> Value { match kind_of(code) { Kind::Real => t.parse::().ok().filter(|v| v.is_finite()).map(Value::Real).unwrap_or(Value::Real(0.0)), Kind::Int16 | Kind::Int32 | Kind::Int64 => { - t.parse::().map(Value::Int).unwrap_or_else(|_| Value::Int(t.parse::().map(|f| f as i64).unwrap_or(0))) + // A float spelling ("1e3", "12.0") is accepted when finite, clamped to the group's + // width; NaN, inf and text become 0. + let (lo, hi) = match kind_of(code) { + Kind::Int16 => (i64::from(i16::MIN), i64::from(i16::MAX)), + Kind::Int32 => (i64::from(i32::MIN), i64::from(i32::MAX)), + _ => (i64::MIN, i64::MAX), + }; + t.parse::() + .map(Value::Int) + .unwrap_or_else(|_| Value::Int(t.parse::().ok().filter(|f| f.is_finite()).map_or(0, |f| (f as i64).clamp(lo, hi)))) } Kind::Bool => Value::Bool(t.parse::().unwrap_or(0) != 0), - Kind::Hex => Value::Hex(t.to_string()), + Kind::Hex => Value::Hex(normalize_hex(t)), Kind::Str => Value::Str(text.trim_end_matches(['\r', '\n']).to_string()), } } const BINARY_SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0"; +const UTF8_BOM: &[u8] = b"\xEF\xBB\xBF"; /// 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; @@ -181,8 +193,10 @@ pub fn parse_with_limit(bytes: &[u8], max_tags: usize) -> Result> { if let Some(body) = bytes.strip_prefix(BINARY_SENTINEL) { return parse_binary_with_limit(body, max_tags); } - // Every group code takes two lines (code, value). - let tags = bytes.iter().filter(|&&b| b == b'\n').count().div_ceil(2); + let bytes = bytes.strip_prefix(UTF8_BOM).unwrap_or(bytes); + // Every group code takes two lines (code, value); a line ends at LF, CRLF or a lone CR. + let ends = bytes.iter().enumerate().filter(|&(i, &b)| b == b'\n' || (b == b'\r' && bytes.get(i + 1) != Some(&b'\n'))).count(); + let tags = ends.div_ceil(2); if tags > max_tags { return Err(DxfError::TooLarge { tags, limit: max_tags }); } @@ -257,6 +271,22 @@ fn unescape_unicode(s: &str) -> String { out } +/// The lines of an ASCII DXF text: LF, CRLF and a lone CR (old Mac files) all end a line, like +/// [`str::lines`] otherwise (no empty line after a final terminator). +fn ascii_lines(text: &str) -> impl Iterator { + let mut rest = text; + std::iter::from_fn(move || { + if rest.is_empty() { + return None; + } + let end = rest.find(['\r', '\n']).unwrap_or(rest.len()); + let line = rest.get(..end).unwrap_or_default(); + let after = rest.get(end..).unwrap_or_default(); + rest = after.strip_prefix("\r\n").or_else(|| after.strip_prefix(['\r', '\n'])).unwrap_or(after); + Some(line) + }) +} + pub fn parse_ascii(text: &str) -> Result> { parse_ascii_with_limit(text, MAX_TAGS) } @@ -264,7 +294,7 @@ pub fn parse_ascii(text: &str) -> Result> { #[allow(clippy::while_let_loop)] fn parse_ascii_with_limit(text: &str, max_tags: usize) -> Result> { let mut tags = Vec::new(); - let mut lines = text.lines().enumerate(); + let mut lines = ascii_lines(text.strip_prefix('\u{feff}').unwrap_or(text)).enumerate(); loop { let Some((ln, code_line)) = lines.next() else { break }; let ct = code_line.trim(); @@ -282,7 +312,7 @@ fn parse_ascii_with_limit(text: &str, max_tags: usize) -> Result> { if tags.len() > max_tags { return Err(DxfError::TooLarge { tags: tags.len(), limit: max_tags }); } - if code == 0 && tags.last().is_some_and(|t| t.str() == "EOF") { + if code == 0 && tags.last().is_some_and(|t| t.str().trim_end() == "EOF") { break; } } @@ -358,7 +388,7 @@ fn parse_binary_with_limit(b: &[u8], max_tags: usize) -> Result> { } let v = match kind_of(code) { Kind::Str => Value::Str(unescape_unicode(&text.string(code, c.cstr()?))), - Kind::Real => Value::Real(c.f64()?), + Kind::Real => Value::Real(Some(c.f64()?).filter(|v| v.is_finite()).unwrap_or(0.0)), Kind::Int16 => Value::Int(i64::from(c.i16()?)), Kind::Int32 => Value::Int(i64::from(c.i32()?)), Kind::Int64 => Value::Int(c.i64()?), @@ -373,7 +403,7 @@ fn parse_binary_with_limit(b: &[u8], max_tags: usize) -> Result> { Value::Hex(s) } }; - let eof = code == 0 && matches!(&v, Value::Str(s) if s == "EOF"); + let eof = code == 0 && matches!(&v, Value::Str(s) if s.trim_end() == "EOF"); tags.push(Tag { code, value: v }); if tags.len() > max_tags { return Err(DxfError::TooLarge { tags: tags.len(), limit: max_tags }); @@ -418,7 +448,22 @@ pub fn write_ascii_as(tags: &[Tag], utf8: bool) -> String { Value::Real(r) => out.push_str(&format_real(*r)), Value::Int(i) => out.push_str(&i.to_string()), Value::Bool(b) => out.push_str(&i64::from(*b).to_string()), - Value::Hex(h) => out.push_str(h), + Value::Hex(h) => { + // The DXF Reference limits a binary chunk line to 254 hex digits (127 bytes); + // longer data continues in further groups of the same code. Only hex digits are + // written, so a hostile value can't inject tags. + let digits = normalize_hex(h); + let mut chunks: Vec<&str> = digits.as_bytes().chunks(254).map(|c| std::str::from_utf8(c).unwrap_or_default()).collect(); + if chunks.is_empty() { + chunks.push(""); + } + for (n, chunk) in chunks.into_iter().enumerate() { + if n > 0 { + let _ = write!(out, "\r\n{:>3}\r\n", t.code); + } + out.push_str(chunk); + } + } } out.push_str("\r\n"); } @@ -460,13 +505,11 @@ pub fn write_binary(tags: &[Tag], utf8: bool) -> Vec { } Kind::Int16 => { out.extend_from_slice(&code_bytes); - let v = t.value.as_i64().clamp(i64::from(i16::MIN), i64::from(i16::MAX)); - out.extend_from_slice(&i16::try_from(v).unwrap_or_default().to_le_bytes()); + out.extend_from_slice(&wrap_i16(t.value.as_i64())); } Kind::Int32 => { out.extend_from_slice(&code_bytes); - let v = t.value.as_i64().clamp(i64::from(i32::MIN), i64::from(i32::MAX)); - out.extend_from_slice(&i32::try_from(v).unwrap_or_default().to_le_bytes()); + out.extend_from_slice(&wrap_i32(t.value.as_i64())); } Kind::Int64 => { out.extend_from_slice(&code_bytes); @@ -494,12 +537,38 @@ pub fn write_binary(tags: &[Tag], utf8: bool) -> Vec { out } -/// The bytes of hex text, two digits each; anything that isn't a pair of hex digits is skipped. +/// Hex text cleaned up: only the hex digits are kept, and an odd trailing digit is padded with +/// `0` (so the bytes before it are never shifted). Used by the reader and both writers. +fn normalize_hex(s: &str) -> String { + let mut out: String = s.chars().filter(char::is_ascii_hexdigit).collect(); + if out.len() % 2 == 1 { + out.push('0'); + } + out +} + +/// The bytes of hex text, two digits each (see [`normalize_hex`]). fn hex_bytes(s: &str) -> Vec { - let digits: Vec = s.bytes().filter_map(|b| char::from(b).to_digit(16).and_then(|d| u8::try_from(d).ok())).collect(); + let digits: Vec = normalize_hex(s).bytes().filter_map(|b| char::from(b).to_digit(16).and_then(|d| u8::try_from(d).ok())).collect(); digits.as_chunks::<2>().0.iter().map(|[hi, lo]| (hi << 4) | lo).collect() } +/// A 16- or 32-bit integer for binary DXF: values that fit the unsigned width wrap into the +/// signed one (65535 is -1, as writers that store unsigned flags expect); anything else clamps. +fn wrap_i16(v: i64) -> [u8; 2] { + match u16::try_from(v) { + Ok(u) => u.to_le_bytes(), + Err(_) => i16::try_from(v.clamp(i64::from(i16::MIN), i64::from(i16::MAX))).unwrap_or_default().to_le_bytes(), + } +} + +fn wrap_i32(v: i64) -> [u8; 4] { + match u32::try_from(v) { + Ok(u) => u.to_le_bytes(), + Err(_) => i32::try_from(v.clamp(i64::from(i32::MIN), i64::from(i32::MAX))).unwrap_or_default().to_le_bytes(), + } +} + /// Whether `bytes` are a binary DXF file (they start with the binary sentinel). pub fn is_binary(bytes: &[u8]) -> bool { bytes.starts_with(BINARY_SENTINEL) @@ -535,12 +604,12 @@ pub fn sections(tags: &[Tag]) -> Vec
{ let mut out = Vec::new(); let mut i = 0; while i < tags.len() { - let is_sec = tags.get(i).is_some_and(|t| t.code == 0 && t.str() == "SECTION"); + let is_sec = tags.get(i).is_some_and(|t| t.code == 0 && t.str().trim_end() == "SECTION"); if is_sec { - let name = tags.get(i + 1).filter(|t| t.code == 2).map(Tag::str).unwrap_or_default(); + let name = tags.get(i + 1).filter(|t| t.code == 2).map(|t| t.str().trim_end().to_string()).unwrap_or_default(); let start = i + 2; let mut j = start; - while j < tags.len() && !(tags.get(j).is_some_and(|t| t.code == 0 && t.str() == "ENDSEC")) { + while j < tags.len() && !(tags.get(j).is_some_and(|t| t.code == 0 && t.str().trim_end() == "ENDSEC")) { j += 1; } out.push(Section { name, tags: tags.get(start..j).unwrap_or(&[]).to_vec() }); diff --git a/crates/dxf/tests/hardening.rs b/crates/dxf/tests/hardening.rs new file mode 100644 index 000000000..24bfea082 --- /dev/null +++ b/crates/dxf/tests/hardening.rs @@ -0,0 +1,167 @@ +//! Hostile-input hardening of the DXF reader and writer (#1025): one regression test per finding. + +use cadcraft_dxf::{Tag, Value, parse, parse_with_limit, sections, write_ascii, write_binary}; + +const SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0"; + +fn bin_with(f: impl FnOnce(&mut Vec)) -> Vec { + let mut b = SENTINEL.to_vec(); + f(&mut b); + b.extend_from_slice(&0i16.to_le_bytes()); + b.extend_from_slice(b"EOF\0"); + b +} + +#[test] +fn binary_reals_that_are_not_finite_become_zero() { + for bits in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + let b = bin_with(|b| { + b.extend_from_slice(&10i16.to_le_bytes()); + b.extend_from_slice(&bits.to_le_bytes()); + }); + let tags = parse(&b).unwrap(); + assert_eq!(tags[0], Tag::f(10, 0.0), "{bits}"); + } +} + +#[test] +fn ascii_accepts_bom_and_cr_only_line_ends() { + let want = vec![Tag::s(0, "SECTION"), Tag::s(2, "ENTITIES"), Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]; + let text = "0\rSECTION\r2\rENTITIES\r0\rENDSEC\r0\rEOF\r"; + assert_eq!(parse(text.as_bytes()).unwrap(), want); + let mut bom = b"\xEF\xBB\xBF".to_vec(); + bom.extend_from_slice(text.replace('\r', "\r\n").as_bytes()); + assert_eq!(parse(&bom).unwrap(), want); + // Mixed terminators. + assert_eq!(parse(b"0\r\nSECTION\r2\nENTITIES\n0\rENDSEC\r\n0\nEOF").unwrap(), want); +} + +#[test] +fn cr_only_files_count_against_the_tag_limit() { + let mut text = String::new(); + for _ in 0..50 { + text.push_str("999\rcomment\r"); + } + text.push_str("0\rEOF\r"); + assert!(parse_with_limit(text.as_bytes(), 10).is_err()); + assert_eq!(parse_with_limit(text.as_bytes(), 100).unwrap().len(), 51); +} + +#[test] +fn one_bad_byte_spoils_one_line_not_the_file() { + // Header says ANSI_1251; one line is UTF-8, one is Windows-1251, one has a stray byte. + let mut b = b"9\n$DWGCODEPAGE\n3\nANSI_1251\n1\n".to_vec(); + b.extend_from_slice("Größe".as_bytes()); + b.extend_from_slice(b"\n1\n\xCF\xF0\xE8\xE2\xE5\xF2\n0\nEOF\n"); + let t = parse(&b).unwrap(); + assert_eq!(t[1].str(), "ANSI_1251"); + assert_eq!(t[2].str(), "Größe"); + assert_eq!(t[3].str(), "Привет"); +} + +#[test] +fn ascii_and_binary_decode_alike() { + // ANSI_1252 file with UTF-8 bytes in one string and a Latin-1 byte in another. + let utf8 = "Größe".as_bytes(); + let latin = b"Caf\xE9"; + let mut a = b"9\n$DWGCODEPAGE\n3\nANSI_1252\n1\n".to_vec(); + a.extend_from_slice(utf8); + a.extend_from_slice(b"\n1\n"); + a.extend_from_slice(latin); + a.extend_from_slice(b"\n0\nEOF\n"); + let from_ascii = parse(&a).unwrap(); + let b = bin_with(|b| { + for (code, s) in [(9i16, &b"$DWGCODEPAGE"[..]), (3, b"ANSI_1252"), (1, utf8), (1, latin)] { + b.extend_from_slice(&code.to_le_bytes()); + b.extend_from_slice(s); + b.push(0); + } + }); + let from_binary = parse(&b).unwrap(); + assert_eq!(from_ascii[2].str(), "Größe"); + assert_eq!(from_ascii[3].str(), "Café"); + assert_eq!(from_binary[2].str(), "Größe"); + assert_eq!(from_binary[3].str(), "Café"); +} + +#[test] +fn trailing_spaces_keep_sections_and_eof() { + let t = parse(b"0\nSECTION \n2\nENTITIES \n0\nLINE\n0\nENDSEC \n0\nEOF \n0\nSECTION\n2\nX\n0\nENDSEC\n").unwrap(); + // The EOF with a trailing space still ends the file. + assert_eq!(t.len(), 5); + let s = sections(&t); + assert_eq!(s.len(), 1); + assert_eq!(s[0].name, "ENTITIES"); + assert_eq!(s[0].tags.len(), 1); + // Binary EOF too. + let mut b = SENTINEL.to_vec(); + b.extend_from_slice(&0i16.to_le_bytes()); + b.extend_from_slice(b"EOF \0"); + b.extend_from_slice(&1i16.to_le_bytes()); + b.extend_from_slice(b"after\0"); + assert_eq!(parse(&b).unwrap().len(), 1); +} + +#[test] +fn integer_groups_reject_non_finite_and_clamp() { + let t = parse(b"70\ninf\n70\nNaN\n70\n1e30\n70\n-1e30\n90\n1e30\n160\n1e30\n160\n-inf\n62\n12.0\n0\nEOF\n").unwrap(); + assert_eq!(t[0], Tag::i(70, 0)); + assert_eq!(t[1], Tag::i(70, 0)); + assert_eq!(t[2], Tag::i(70, i64::from(i16::MAX))); + assert_eq!(t[3], Tag::i(70, i64::from(i16::MIN))); + assert_eq!(t[4], Tag::i(90, i64::from(i32::MAX))); + assert_eq!(t[5], Tag::i(160, i64::MAX)); + assert_eq!(t[6], Tag::i(160, 0)); + assert_eq!(t[7], Tag::i(62, 12)); + assert_eq!(Value::Str("inf".into()).as_i64(), 0); +} + +#[test] +fn hex_groups_pad_odd_digits_and_drop_junk() { + let t = parse(b"310\nABC\n310\nA-B zC1\n310\n\n1004\n0aF\n0\nEOF\n").unwrap(); + assert_eq!(t[0].value, Value::Hex("ABC0".into())); + assert_eq!(t[1].value, Value::Hex("ABC1".into())); + assert_eq!(t[2].value, Value::Hex(String::new())); + assert_eq!(t[3].value, Value::Hex("0aF0".into())); + // The binary writer agrees with the reader: the odd digit is padded, nothing shifts. + let back = parse(&write_binary(&[Tag::new(310, Value::Hex("ABC".into())), Tag::s(0, "EOF")], true)).unwrap(); + assert_eq!(back[0].value, Value::Hex("ABC0".into())); +} + +#[test] +fn ascii_hex_cannot_inject_tags_and_is_split_at_254_digits() { + let evil = Tag::new(310, Value::Hex("AB\r\n0\r\nEOF".into())); + let text = write_ascii(&[evil, Tag::s(0, "EOF")]); + let back = parse(text.as_bytes()).unwrap(); + assert_eq!(back.len(), 2, "{text:?}"); + assert_eq!(back[0].value, Value::Hex("AB0EF0".into())); + let long = "AB".repeat(300); + let back = parse(write_ascii(&[Tag::new(310, Value::Hex(long)), Tag::s(0, "EOF")]).as_bytes()).unwrap(); + let sizes: Vec = back.iter().filter(|t| t.code == 310).map(|t| t.str().len()).collect(); + assert_eq!(sizes, vec![254, 254, 92]); + // An empty chunk is still one group. + let back = parse(write_ascii(&[Tag::new(310, Value::Hex(String::new())), Tag::s(0, "EOF")]).as_bytes()).unwrap(); + assert_eq!(back[0].code, 310); +} + +#[test] +fn binary_sixteen_and_thirty_two_bit_values_wrap_in_range_and_clamp_outside() { + let tags = [ + Tag::i(70, 65535), + Tag::i(70, 40000), + Tag::i(70, 70000), + Tag::i(70, -40000), + Tag::i(90, 0xFFFF_FFFF), + Tag::i(90, 1 << 40), + Tag::i(90, -(1 << 40)), + Tag::s(0, "EOF"), + ]; + let back = parse(&write_binary(&tags, true)).unwrap(); + assert_eq!(back[0], Tag::i(70, -1)); + assert_eq!(back[1], Tag::i(70, 40000 - 65536)); + assert_eq!(back[2], Tag::i(70, i64::from(i16::MAX))); + assert_eq!(back[3], Tag::i(70, i64::from(i16::MIN))); + assert_eq!(back[4], Tag::i(90, -1)); + assert_eq!(back[5], Tag::i(90, i64::from(i32::MAX))); + assert_eq!(back[6], Tag::i(90, i64::from(i32::MIN))); +} diff --git a/crates/io/tests/codepages.rs b/crates/io/tests/codepages.rs index 40d85b3f4..64664f5bc 100644 --- a/crates/io/tests/codepages.rs +++ b/crates/io/tests/codepages.rs @@ -156,8 +156,10 @@ fn binary_dxf_honours_the_code_page() { let read = |b: &[u8]| text_of(&cadcraft_io::read_dxf(b).unwrap()); assert_eq!(read(&bin("AC1015", "ANSI_1251", b"\xCF\xF0\xE8\xE2\xE5\xF2")), "Привет"); assert_eq!(read(&bin("AC1018", "ANSI_932", b"\x95\x5C\\U+00E9")), "表é"); - // A short Cyrillic string that is also valid UTF-8 (D1 B2) is still read in the code page. - assert_eq!(read(&bin("AC1015", "ANSI_1251", b"\xD1\xB2")), "СІ"); + // A string that is valid UTF-8 (D1 B2) is read as UTF-8 in binary files too, the same policy + // as ASCII files (#1025); a code-page string with a non-UTF-8 byte still uses the code page. + assert_eq!(read(&bin("AC1015", "ANSI_1251", b"\xD1\xB2")), "\u{0472}"); + assert_eq!(read(&bin("AC1015", "ANSI_1252", "Größe".as_bytes())), "Größe"); assert_eq!(read(&bin("AC1021", "ANSI_1251", "Привет".as_bytes())), "Привет"); assert_eq!(read(&bin("AC1015", "ANSI_1361", b"Caf\xE9")), "Café"); }