mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 20:10:12 -06:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e707ab4d7c
commit
043073808f
+27
-10
@@ -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() {
|
||||
|
||||
+88
-19
@@ -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::<i64>().unwrap_or_else(|_| s.trim().parse::<f64>().map(|f| f as i64).unwrap_or(0)),
|
||||
Value::Str(s) => {
|
||||
s.trim().parse::<i64>().unwrap_or_else(|_| s.trim().parse::<f64>().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::<f64>().ok().filter(|v| v.is_finite()).map(Value::Real).unwrap_or(Value::Real(0.0)),
|
||||
Kind::Int16 | Kind::Int32 | Kind::Int64 => {
|
||||
t.parse::<i64>().map(Value::Int).unwrap_or_else(|_| Value::Int(t.parse::<f64>().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::<i64>()
|
||||
.map(Value::Int)
|
||||
.unwrap_or_else(|_| Value::Int(t.parse::<f64>().ok().filter(|f| f.is_finite()).map_or(0, |f| (f as i64).clamp(lo, hi))))
|
||||
}
|
||||
Kind::Bool => Value::Bool(t.parse::<i64>().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<Vec<Tag>> {
|
||||
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<Item = &str> {
|
||||
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<Vec<Tag>> {
|
||||
parse_ascii_with_limit(text, MAX_TAGS)
|
||||
}
|
||||
@@ -264,7 +294,7 @@ pub fn parse_ascii(text: &str) -> Result<Vec<Tag>> {
|
||||
#[allow(clippy::while_let_loop)]
|
||||
fn parse_ascii_with_limit(text: &str, max_tags: usize) -> Result<Vec<Tag>> {
|
||||
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<Vec<Tag>> {
|
||||
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<Vec<Tag>> {
|
||||
}
|
||||
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<Vec<Tag>> {
|
||||
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<u8> {
|
||||
}
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
let digits: Vec<u8> = s.bytes().filter_map(|b| char::from(b).to_digit(16).and_then(|d| u8::try_from(d).ok())).collect();
|
||||
let digits: Vec<u8> = 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<Section> {
|
||||
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() });
|
||||
|
||||
@@ -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<u8>)) -> Vec<u8> {
|
||||
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<usize> = 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)));
|
||||
}
|
||||
@@ -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é");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user