Files
Dylan M. Taylor c516452b47 craft-codepage: one table per legacy code page, for craft-rtf, craft-dxf and craft-mime (#991)
craft-rtf and craft-dxf each carried tables for DOS 437, 737, 850, 852 and
857 (and craft-rtf one for 866). They are now one set in craft-codepage,
which takes craft-rtf's codepage module (Windows and Mac tables, OEM glyphs,
font charsets) and craft-dxf's DOS tables. craft_rtf::codepage re-exports it
unchanged; craft-dxf keeps its $DWGCODEPAGE names and Decoder and looks the
DOS tables up there.

Every byte of every code page either crate decodes is unchanged: the
public decoders of both crates were dumped over all 256 bytes of each page
(and the East Asian pairs) before and after, and the dumps are identical.

craft-mime now reads the DOS pages by their IANA names (IBM437, cp850,
ibm-852, 857...), which it read as UTF-8 before.
2026-11-11 20:17:35 -05:00

1338 lines
52 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A standalone DXF tag reader and writer.
//!
//! DXF is a sequence of (group code, value) pairs. This crate parses ASCII and binary DXF into a
//! flat list of [`Tag`]s, splits them into sections and entities, and writes tags back. [`model`]
//! reads the tags as a drawing (header variables, tables, blocks, entities, objects); mapping
//! that onto an application's own document is left to the caller. Written from Autodesk's
//! published DXF Reference; depends on no other Craft crate. Moved from CADCraft's `cadcraft-dxf`.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
use std::io::Write as _;
pub mod model;
mod version;
pub use version::Version;
#[derive(Debug, thiserror::Error)]
pub enum DxfError {
#[error("not a DXF file")]
NotDxf,
#[error("line {line}: bad group code `{text}`")]
BadCode { line: usize, text: String },
#[error("unexpected end of file")]
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 (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<T> = std::result::Result<T, DxfError>;
/// A typed group value.
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Str(String),
Real(f64),
Int(i64),
Bool(bool),
/// Binary chunk (310–319, 1004) as hex text.
Hex(String),
}
impl Value {
pub fn as_str(&self) -> String {
match self {
Value::Str(s) | Value::Hex(s) => s.clone(),
Value::Real(r) => format_real(*r),
Value::Int(i) => i.to_string(),
Value::Bool(b) => i64::from(*b).to_string(),
}
}
#[expect(
clippy::cast_precision_loss,
reason = "DXF numeric accessors intentionally convert integer values to f64, accepting precision loss beyond 2^53."
)]
pub fn as_f64(&self) -> f64 {
match self {
Value::Real(r) if r.is_finite() => *r,
Value::Int(i) => *i as f64,
Value::Str(s) => s.trim().parse::<f64>().ok().filter(|v| v.is_finite()).unwrap_or(0.0),
Value::Bool(b) => f64::from(u8::from(*b)),
_ => 0.0,
}
}
#[expect(clippy::cast_possible_truncation, reason = "DXF numeric accessors intentionally truncate finite real values to i64.")]
pub fn as_i64(&self) -> i64 {
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>().ok().filter(|f| f.is_finite()).map_or(0, |f| f as i64))
}
Value::Bool(b) => i64::from(*b),
_ => 0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tag {
pub code: i32,
pub value: Value,
}
impl Tag {
pub fn new(code: i32, value: Value) -> Self {
Tag { code, value }
}
pub fn s(code: i32, v: impl Into<String>) -> Self {
Tag { code, value: Value::Str(v.into()) }
}
pub fn f(code: i32, v: f64) -> Self {
Tag { code, value: Value::Real(if v.is_finite() { v } else { 0.0 }) }
}
pub fn i(code: i32, v: i64) -> Self {
Tag { code, value: Value::Int(v) }
}
pub fn str(&self) -> String {
self.value.as_str()
}
/// The value when it is text, without a copy.
pub fn str_ref(&self) -> Option<&str> {
match &self.value {
Value::Str(s) | Value::Hex(s) => Some(s),
_ => None,
}
}
pub fn f64(&self) -> f64 {
self.value.as_f64()
}
pub fn i64(&self) -> i64 {
self.value.as_i64()
}
}
/// The value type for a group code (DXF Reference, "Group Code Value Types").
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Str,
Real,
Int16,
Int32,
Int64,
Bool,
Hex,
}
pub fn kind_of(code: i32) -> Kind {
match code {
10..=59 | 110..=149 | 210..=239 | 460..=469 | 1010..=1059 => Kind::Real,
60..=79 | 170..=179 | 270..=289 | 370..=389 | 400..=409 | 1060..=1070 => Kind::Int16,
90..=99 | 420..=429 | 440..=449 | 450..=459 | 1071 => Kind::Int32,
160..=169 => Kind::Int64,
290..=299 => Kind::Bool,
310..=319 | 1004 => Kind::Hex,
_ => Kind::Str,
}
}
#[expect(clippy::cast_possible_truncation, reason = "DXF numeric parsing intentionally truncates finite real values to i64.")]
fn typed(code: i32, text: &str) -> Value {
let t = text.trim();
match kind_of(code) {
Kind::Real => t.parse::<f64>().ok().filter(|v| v.is_finite()).map_or(Value::Real(0.0), Value::Real),
Kind::Int16 | Kind::Int32 | Kind::Int64 => {
// A float spelling ("1e3", "12.0") is accepted when finite, clamped to what the
// group's width holds, signed or unsigned (files write 16-bit flags up to 65535, as
// the integer spelling reads them); NaN, inf and text become 0.
let (lo, hi) = match kind_of(code) {
Kind::Int16 => (i64::from(i16::MIN), i64::from(u16::MAX)),
Kind::Int32 => (i64::from(i32::MIN), i64::from(u32::MAX)),
_ => (i64::MIN, i64::MAX),
};
t.parse::<i64>()
.map_or_else(|_| Value::Int(t.parse::<f64>().ok().filter(|f| f.is_finite()).map_or(0, |f| (f as i64).clamp(lo, hi))), Value::Int)
}
Kind::Bool => Value::Bool(t.parse::<i64>().unwrap_or(0) != 0),
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 by default. A `Tag` takes 40 bytes plus its text, so this
/// 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";
/// 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::<usize>().ok()).filter(|&n| n > 0).unwrap_or(MAX_TAGS)
}
mod codepage;
pub use codepage::decode_code_page;
/// Parse a DXF file (ASCII or binary).
///
/// # Errors
///
/// Returns an error for empty input, an invalid group code, an incomplete group-code/value pair,
/// truncated binary data, or more than [`MAX_TAGS`] group codes. For ASCII input, invalid numeric
/// text becomes `0.0` for real values or `0` for integer values, and an EOF tag is not required.
pub fn parse(bytes: &[u8]) -> Result<Vec<Tag>> {
parse_with_limit(bytes, max_tags())
}
/// [`parse`] for a file that may be cut short or damaged: an ASCII file is read up to the first
/// line that is no group code (or a last group code with no value), and what came before is
/// returned with the 1-based number of that line, so a caller can open what the file still holds
/// and say where it stops. `None` for a file read to its end. A truncated binary file, an empty
/// one, one damaged before its first group, and one over the limit are still errors.
///
/// # Errors
///
/// As [`parse`], except for damage after the first group of an ASCII file.
pub fn parse_recovering(bytes: &[u8]) -> Result<(Vec<Tag>, Option<usize>)> {
if let Some(body) = bytes.strip_prefix(BINARY_SENTINEL) {
return parse_binary_with_limit(body, max_tags()).map(|tags| (tags, None));
}
let bytes = bytes.strip_prefix(UTF8_BOM).unwrap_or(bytes);
let (tags, limit) = (count_ascii_pairs(bytes), max_tags());
if tags > limit {
return Err(DxfError::TooLarge { tags, limit });
}
ascii_tags(&codepage::decode_ascii_file(bytes), limit, tags, true)
}
/// The nonblank group-code/value pairs of an ASCII file (LF, CRLF and a lone CR each end a
/// line), up to EOF just as the parser stops there. Blank lines do not count.
fn count_ascii_pairs(bytes: &[u8]) -> usize {
let mut lines = byte_lines(bytes);
let mut tags = 0;
while let Some(code) = lines.next() {
let code = code.trim_ascii();
if code.is_empty() {
continue;
}
let Some(value) = lines.next() else { break };
tags += 1;
if code == b"0" && value.trim_ascii() == b"EOF" {
break;
}
}
tags
}
/// [`parse`] with an explicit group-code limit. For ASCII DXF the limit is checked up front from
/// the line count, before the text is decoded or any tag allocated, so an oversized file fails in
/// milliseconds instead of after it has filled memory.
///
/// # Errors
///
/// Returns an error for empty input, an invalid group code, an incomplete group-code/value pair,
/// truncated binary data, or more than `max_tags` group codes. For ASCII input, invalid numeric
/// text becomes `0.0` for real values or `0` for integer values, and an EOF tag is not required.
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);
}
let bytes = bytes.strip_prefix(UTF8_BOM).unwrap_or(bytes);
// Counted first, before the text is decoded or any tag allocated.
let tags = count_ascii_pairs(bytes);
if tags > max_tags {
return Err(DxfError::TooLarge { tags, limit: 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.
fn escape_at(chars: &[char], i: usize) -> Option<u32> {
if chars.get(i) != Some(&'\\') || chars.get(i + 1) != Some(&'U') || chars.get(i + 2) != Some(&'+') {
return None;
}
let hex = chars.get(i + 3..i + 7)?;
if !hex.iter().all(char::is_ascii_hexdigit) {
return None;
}
hex.iter().try_fold(0u32, |acc, c| Some(acc * 16 + c.to_digit(16)?))
}
/// The character of a `\M+nXXXX` escape starting at `chars[i]`, if there is one: `n` names a
/// double-byte code page and `XXXX` is the character's two bytes in hex.
pub fn mif_escape_at(chars: &[char], i: usize) -> Option<char> {
if chars.get(i) != Some(&'\\') || chars.get(i + 1) != Some(&'M') || chars.get(i + 2) != Some(&'+') {
return None;
}
let n = *chars.get(i + 3)?;
let hex = chars.get(i + 4..i + 8)?;
if !hex.iter().all(char::is_ascii_hexdigit) {
return None;
}
let code = hex.iter().try_fold(0u16, |acc, c| Some(acc * 16 + u16::try_from(c.to_digit(16)?).ok()?))?;
codepage::mif_char(n, code.to_be_bytes())
}
/// Unescape the `\U+XXXX` sequences pre-R2007 DXF uses for characters outside the file's code
/// page (four hex digits, one UTF-16 code unit; characters above U+FFFF are a surrogate pair of
/// escapes), and the `\M+nXXXX` sequences for double-byte East Asian characters. Malformed
/// escapes, unpaired surrogates and unknown `\M+` code pages are kept as literal text.
fn unescape_unicode(s: &str) -> String {
if !has_escape(s) {
return s.to_string();
}
unescape_found(s)
}
/// [`unescape_unicode`] for a string the caller owns: without escapes (nearly always) it is
/// returned as it is, with no copy.
fn unescape_owned(s: String) -> String {
if has_escape(&s) { unescape_found(&s) } else { s }
}
fn has_escape(s: &str) -> bool {
s.contains("\\U+") || s.contains("\\M+")
}
/// The longest escape: a surrogate pair of two `\U+XXXX` (14 characters).
const MAX_ESCAPE_CHARS: usize = 14;
fn unescape_found(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
// Everything up to the next backslash is copied as it is; an escape is looked for in a window
// of the next few characters only, so no copy of the whole string (4 bytes a character) is made.
while let Some(at) = rest.find('\\') {
let (plain, tail) = rest.split_at(at);
out.push_str(plain);
let mut buf = ['\0'; MAX_ESCAPE_CHARS];
let mut n = 0;
for (slot, c) in buf.iter_mut().zip(tail.chars()) {
*slot = c;
n += 1;
}
let window = buf.get(..n).unwrap_or_default();
let (ch, used) = if let Some(ch) = mif_escape_at(window, 0) {
(Some(ch), 8)
} else if let Some(unit) = escape_at(window, 0) {
if let Some(ch) = char::from_u32(unit) {
(Some(ch), 7)
} else if (0xD800..0xDC00).contains(&unit)
&& let Some(low) = escape_at(window, 7).filter(|u| (0xDC00..0xE000).contains(u))
{
(char::from_u32(0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)), 14)
} else {
(None, 0)
}
} else {
(None, 0)
};
match ch {
Some(ch) => {
out.push(ch);
// Every escape character is ASCII, so `used` characters are `used` bytes.
rest = tail.get(used..).unwrap_or_default();
}
None => {
out.push('\\');
rest = tail.get(1..).unwrap_or_default();
}
}
}
out.push_str(rest);
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)
})
}
/// Byte-level twin of [`ascii_lines`] for the up-front limit check, so the count matches the parser.
fn byte_lines(mut rest: &[u8]) -> impl Iterator<Item = &[u8]> {
std::iter::from_fn(move || {
if rest.is_empty() {
return None;
}
let end = rest.iter().position(|&b| b == b'\r' || b == b'\n').unwrap_or(rest.len());
let line = &rest[..end];
let after = &rest[end..];
rest = match after {
[b'\r', b'\n', tail @ ..] | [b'\r' | b'\n', tail @ ..] => tail,
_ => after,
};
Some(line)
})
}
/// Parse ASCII DXF text into typed tags.
///
/// # Errors
///
/// Returns an error for empty input, an invalid group code, an incomplete group-code/value pair,
/// or too many tags. Invalid numeric text becomes `0.0` for real values or `0` for integer values.
/// A complete tag stream does not require an EOF tag.
pub fn parse_ascii(text: &str) -> Result<Vec<Tag>> {
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.
fn parse_ascii_counted(text: &str, max_tags: usize, expected: usize) -> Result<Vec<Tag>> {
ascii_tags(text, max_tags, expected, false).map(|(tags, _)| tags)
}
/// The tags of ASCII DXF text. With `recover`, a line that is no group code, or a group code
/// with no value after it, ends the reading instead of failing it: the tags before it are
/// returned with the 1-based number of that line.
#[allow(clippy::while_let_loop)]
fn ascii_tags(text: &str, max_tags: usize, expected: usize, recover: bool) -> Result<(Vec<Tag>, Option<usize>)> {
let mut tags = Vec::new();
tags.try_reserve_exact(expected.min(max_tags)).map_err(|_| DxfError::OutOfMemory { tags: expected })?;
let mut lines = ascii_lines(text.strip_prefix('\u{feff}').unwrap_or(text)).enumerate();
let mut damaged = None;
loop {
let Some((ln, code_line)) = lines.next() else { break };
let ct = code_line.trim();
if ct.is_empty() {
continue;
}
let Ok(code) = ct.parse::<i32>() else {
if recover && !tags.is_empty() {
damaged = Some(ln + 1);
break;
}
return Err(DxfError::BadCode { line: ln + 1, text: ct.chars().take(40).collect() });
};
let Some((_, val)) = lines.next() else {
if recover && !tags.is_empty() {
damaged = Some(ln + 1);
break;
}
return Err(DxfError::Eof);
};
let v = match typed(code, val) {
// Only text with a backslash can hold an escape; the rest moves in as it is.
Value::Str(s) => Value::Str(unescape_owned(s)),
o => o,
};
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 });
}
if eof {
break;
}
}
if tags.is_empty() {
return Err(DxfError::NotDxf);
}
Ok((tags, damaged))
}
struct Cur<'a> {
b: &'a [u8],
p: usize,
}
impl<'a> Cur<'a> {
fn take(&mut self, n: usize) -> Result<&[u8]> {
let end = self.p.checked_add(n).ok_or(DxfError::Eof)?;
let s = self.b.get(self.p..end).ok_or(DxfError::Eof)?;
self.p = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
Ok(*self.take(1)?.first().ok_or(DxfError::Eof)?)
}
fn i16(&mut self) -> Result<i16> {
let s = self.take(2)?;
Ok(i16::from_le_bytes([s[0], s[1]]))
}
fn i32(&mut self) -> Result<i32> {
let s = self.take(4)?;
Ok(i32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn i64(&mut self) -> Result<i64> {
let s = self.take(8)?;
let mut a = [0u8; 8];
a.copy_from_slice(s);
Ok(i64::from_le_bytes(a))
}
fn f64(&mut self) -> Result<f64> {
let s = self.take(8)?;
let mut a = [0u8; 8];
a.copy_from_slice(s);
Ok(f64::from_le_bytes(a))
}
/// The bytes of a zero-terminated string (without the zero), decoded by the caller.
fn cstr(&mut self) -> Result<&'a [u8]> {
let rest = self.b.get(self.p..).unwrap_or(&[]);
let len = rest.iter().position(|&b| b == 0).ok_or(DxfError::Eof)?;
self.p += len + 1;
Ok(rest.get(..len).unwrap_or(&[]))
}
}
const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
/// Binary DXF, the body after the sentinel: R13+ (2-byte group codes) or R12 and earlier (1-byte
/// group codes, where 255 is followed by a 2-byte code).
///
/// # Errors
///
/// Returns an error for an empty or malformed file, an unexpected end of file, or too many tags.
pub fn parse_binary(b: &[u8]) -> Result<Vec<Tag>> {
parse_binary_with_limit(b, max_tags())
}
/// Whether a binary DXF body uses the pre-R13 layout with 1-byte group codes, told from its first
/// tag: R13+ starts with a 2-byte code (`00 00` for the usual `0 SECTION`, `E7 03` for a 999
/// comment), the old layout with a 1-byte `00` followed by the string's first character, or with
/// the 255 escape and a code that needs it (255 to 1071; an R13+ file whose first group is code 255
/// has the string's first character there instead).
fn legacy_binary_layout(b: &[u8]) -> bool {
match b {
[0, next, ..] => *next != 0,
[255, lo, hi, ..] => (255..=1071).contains(&u16::from_le_bytes([*lo, *hi])),
_ => false,
}
}
fn parse_binary_with_limit(b: &[u8], max_tags: usize) -> Result<Vec<Tag>> {
let mut text = codepage::BinaryText::default();
let tags = parse_binary_pass(b, max_tags, &mut text)?;
if !text.needs_code_page_pass() {
return Ok(tags);
}
// The first pass's tags go before the second pass runs: both held at once would double
// the peak near the tag limit.
drop(tags);
parse_binary_pass(b, max_tags, &mut codepage::BinaryText::code_page_pass())
}
fn parse_binary_pass(b: &[u8], max_tags: usize, text: &mut codepage::BinaryText) -> Result<Vec<Tag>> {
let mut c = Cur { b, p: 0 };
let mut tags = Vec::new();
let legacy = legacy_binary_layout(b);
while c.p < b.len() {
// 255 escapes a 2-byte code only in the 1-byte layout; in R13+ it is an ordinary code.
let code = if legacy {
match c.u8()? {
255 => i32::from(c.i16()?),
x => i32::from(x),
}
} else {
i32::from(c.i16()?)
};
let v = match kind_of(code) {
Kind::Str => Value::Str(unescape_unicode(&text.string(code, c.cstr()?))),
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()?),
Kind::Bool => Value::Bool(c.u8()? != 0),
Kind::Hex => {
let n = usize::from(c.u8()?);
let bytes = c.take(n)?;
let mut s = String::with_capacity(n * 2);
for &x in bytes {
s.push(char::from(HEX_DIGITS[usize::from(x >> 4)]));
s.push(char::from(HEX_DIGITS[usize::from(x & 15)]));
}
Value::Hex(s)
}
};
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 });
}
if eof {
break;
}
}
if tags.is_empty() {
return Err(DxfError::NotDxf);
}
Ok(tags)
}
/// Format a real the way DXF writers usually do (shortest round-trip, with a decimal point).
pub fn format_real(v: f64) -> String {
let mut out = Vec::new();
push_real(&mut out, v);
// What `push_real` writes is ASCII.
String::from_utf8(out).unwrap_or_default()
}
/// [`format_real`] appended to `out`, without a string of its own (a large drawing writes
/// millions of reals).
fn push_real(out: &mut Vec<u8>, v: f64) {
if !v.is_finite() {
out.extend_from_slice(b"0.0");
return;
}
// Very large and very small values in exponent form, as AutoCAD writes them (`1.0E+20`):
// plain digits would run to hundreds of characters.
let a = v.abs();
if a != 0.0 && !(1e-10..1e16).contains(&a) {
let e = format!("{v:E}");
let (m, x) = e.split_once('E').unwrap_or((&e, "0"));
let x: i32 = x.parse().unwrap_or(0);
let point = if m.contains('.') { "" } else { ".0" };
let _ = write!(out, "{m}{point}E{}{:02}", if x < 0 { '-' } else { '+' }, x.abs());
return;
}
let start = out.len();
let _ = write!(out, "{v}");
// A finite real in this range prints as an optional sign, digits and an optional point.
if !out.get(start..).is_some_and(|s| s.contains(&b'.')) {
out.extend_from_slice(b".0");
}
}
/// A group code right-aligned to three characters and its line end, as `write!("{:>3}\r\n")`
/// writes it, without the formatting machinery for the codes DXF has (0 to 9999).
fn push_code(out: &mut Vec<u8>, code: i32) {
match u16::try_from(code) {
Ok(c) if c < 10_000 => {
let digits = [c / 1000, c / 100 % 10, c / 10 % 10, c % 10].map(|d| b'0' + u8::try_from(d % 10).unwrap_or(0));
// The significant digits (at least one), padded to three.
let first = digits.iter().position(|&d| d != b'0').unwrap_or(3);
for _ in 1..first {
out.push(b' ');
}
out.extend_from_slice(digits.get(first..).unwrap_or_default());
out.extend_from_slice(b"\r\n");
}
_ => {
let _ = write!(out, "{code:>3}\r\n");
}
}
}
/// Most characters one string group holds: longer text is split over several groups (MTEXT
/// contents, field codes).
pub const MAX_STRING_CHARS: usize = 250;
/// `s` split into strings of at most [`MAX_STRING_CHARS`] characters, in order; none for an
/// empty string.
pub fn string_groups(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut n = 0;
for c in s.chars() {
if n == MAX_STRING_CHARS {
out.push(std::mem::take(&mut cur));
n = 0;
}
cur.push(c);
n += 1;
}
if !cur.is_empty() {
out.push(cur);
}
out
}
/// Write tags as ASCII DXF (CRLF line ends, codes right-aligned to 3 as is conventional).
///
/// The output is pure ASCII: non-ASCII characters in strings are written as `\U+XXXX` escapes,
/// the pre-R2007 convention, so it is correct whatever `$DWGCODEPAGE` the header declares.
pub fn write_ascii(tags: &[Tag]) -> String {
write_ascii_as(tags, false)
}
/// Write tags as ASCII DXF with text in UTF-8 (`utf8`, the R2007+ convention: non-ASCII
/// characters as they are) or escaped as `\U+XXXX` (older versions, see [`write_ascii`]). Line
/// breaks inside strings become spaces either way.
pub fn write_ascii_as(tags: &[Tag], utf8: bool) -> String {
let bytes = ascii(tags, if utf8 { Strings::Utf8 } else { Strings::Escaped(None) });
// Both forms are UTF-8 (the escaped one is ASCII).
String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
/// 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 this crate doesn't know, `ASCII`, and `UTF8`/`UNICODE` (not a pre-2007 code page)
/// give [`write_ascii`]'s pure ASCII output. So does a file whose code page bytes happen to be
/// valid UTF-8 (`СІ` in ANSI_1251 is D1 B2, U+0472), since readers, ours included, take an ASCII
/// DXF that is valid UTF-8 as UTF-8.
pub fn write_ascii_in(tags: &[Tag], code_page: &str) -> Vec<u8> {
let bytes = write_ascii_in_page(tags, code_page);
if !bytes.is_ascii() && std::str::from_utf8(&bytes).is_ok() {
return ascii(tags, Strings::Escaped(None));
}
bytes
}
/// [`write_ascii_in`] without its fallback to pure ASCII: the text is in the code page even when
/// its bytes happen to be valid UTF-8. For a reader that decodes a pre-2007 DXF by the
/// `$DWGCODEPAGE` it declares and never guesses UTF-8, as the DWG bridge's acadrust does. A code
/// page this crate doesn't know, `ASCII`, and `UTF8`/`UNICODE` still give pure ASCII.
pub fn write_ascii_in_page(tags: &[Tag], code_page: &str) -> Vec<u8> {
ascii(tags, Strings::Escaped(codepage::encoder(code_page)))
}
/// How a writer stores string values.
#[derive(Clone, Copy)]
enum Strings {
/// As they are (R2007+).
Utf8,
/// ASCII as it is, other characters in this code page when it has them, otherwise as
/// `\U+XXXX` escapes (before R2007).
Escaped(Option<codepage::Decoder>),
}
fn ascii(tags: &[Tag], strings: Strings) -> Vec<u8> {
let mut out = Vec::with_capacity(tags.len() * 16);
for t in tags {
push_code(&mut out, t.code);
match &t.value {
// Most text has no line breaks or backslashes and is ASCII (or goes out as UTF-8):
// copied as it is. A code page may write an ASCII character another way, so text
// for one takes the long path.
Value::Str(s)
if !s.contains(['\r', '\n', '\\'])
&& (matches!(strings, Strings::Utf8) || (matches!(strings, Strings::Escaped(None)) && s.is_ascii())) =>
{
out.extend_from_slice(s.as_bytes());
}
Value::Str(s) => push_string(s, strings, &mut out),
Value::Real(r) => push_real(&mut out, *r),
Value::Int(i) => {
let _ = write!(out, "{i}");
}
Value::Bool(b) => out.push(if *b { b'1' } else { b'0' }),
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.extend_from_slice(chunk.as_bytes());
}
}
}
out.extend_from_slice(b"\r\n");
}
out
}
/// Write tags as binary DXF (DXF Reference, "Binary DXF Files", R13+ layout): the sentinel,
/// then per tag a 2-byte little-endian group code and the value in the binary form of the code's
/// [`Kind`]: a zero-terminated string, an 8-byte real, a 2-, 4- or 8-byte integer, a 1-byte
/// boolean, or a binary chunk (one length byte, then up to 255 bytes; longer data is split into
/// several groups of the same code). Strings are UTF-8 when `utf8` (R2007+), otherwise escaped as
/// in [`write_ascii`]. A tag's value is converted to its code's kind, so a number written as text
/// is stored as a number; a code outside the 16-bit range is skipped.
pub fn write_binary(tags: &[Tag], utf8: bool) -> Vec<u8> {
binary(tags, if utf8 { Strings::Utf8 } else { Strings::Escaped(None) })
}
/// 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<u8> {
binary(tags, Strings::Escaped(codepage::encoder(code_page)))
}
fn binary(tags: &[Tag], strings: Strings) -> Vec<u8> {
let mut out = Vec::with_capacity(BINARY_SENTINEL.len() + tags.len() * 12);
out.extend_from_slice(BINARY_SENTINEL);
for t in tags {
let Ok(code) = i16::try_from(t.code) else { continue };
let code_bytes = code.to_le_bytes();
match kind_of(t.code) {
Kind::Str => {
out.extend_from_slice(&code_bytes);
let s = match &t.value {
Value::Str(s) => s.clone(),
v => v.as_str(),
};
let mut text = Vec::with_capacity(s.len());
push_string(&s, strings, &mut text);
out.extend(text.into_iter().filter(|&b| b != 0));
out.push(0);
}
Kind::Real => {
out.extend_from_slice(&code_bytes);
out.extend_from_slice(&t.value.as_f64().to_le_bytes());
}
Kind::Int16 => {
out.extend_from_slice(&code_bytes);
out.extend_from_slice(&wrap_i16(t.value.as_i64()));
}
Kind::Int32 => {
out.extend_from_slice(&code_bytes);
out.extend_from_slice(&wrap_i32(t.value.as_i64()));
}
Kind::Int64 => {
out.extend_from_slice(&code_bytes);
out.extend_from_slice(&t.value.as_i64().to_le_bytes());
}
Kind::Bool => {
out.extend_from_slice(&code_bytes);
out.push(u8::from(t.value.as_i64() != 0));
}
Kind::Hex => {
let bytes = hex_bytes(&t.value.as_str());
// An empty chunk is still one group, as in ASCII.
let mut chunks: Vec<&[u8]> = bytes.chunks(255).collect();
if chunks.is_empty() {
chunks.push(&[]);
}
for chunk in chunks {
out.extend_from_slice(&code_bytes);
out.push(u8::try_from(chunk.len()).unwrap_or(u8::MAX));
out.extend_from_slice(chunk);
}
}
}
}
out
}
/// 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> = 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)
}
/// A string value as `strings` stores it. Line breaks become spaces. Before R2007 the text is
/// in the `$DWGCODEPAGE` code page: ASCII stays as it is, a character the code page has becomes
/// its bytes, and every other one `\U+XXXX` (a surrogate pair of escapes above U+FFFF). Without
/// a code page the result is ASCII, so it is valid in any code page. In every form a backslash
/// that would start a `\U+` or `\M+` sequence is written as `\U+005C`, so the reader gives back
/// the literal text (#2050). So is a double-byte character whose trail byte is 0x5C (`\`) in that
/// place, such as Shift-JIS `表` or Big5 `許`: CADCraft decodes the code page before it looks for
/// escapes, but a reader that looks first would take that byte and the `U+` after it for one.
fn push_string(s: &str, strings: Strings, out: &mut Vec<u8>) {
let chars: Vec<char> = s.chars().collect();
let escape_at = |j: usize| matches!(chars.get(j), Some('U' | 'M')) && chars.get(j + 1) == Some(&'+');
for (i, &c) in chars.iter().enumerate() {
match (c, strings) {
('\r' | '\n', _) => out.push(b' '),
('\\', _) if escape_at(i + 1) => {
out.extend_from_slice(b"\\U+005C");
}
(c, Strings::Utf8) => out.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes()),
(c, Strings::Escaped(Some(d))) if d.shadows_ascii(c) => {
let _ = write!(out, "\\U+{:04X}", u32::from(c));
}
(c, _) if c.is_ascii() => out.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes()),
(c, Strings::Escaped(Some(d))) if code_page_bytes(c, d, escape_at(i + 1), out) => {}
(c, Strings::Escaped(_)) => {
let mut units = [0u16; 2];
for u in c.encode_utf16(&mut units) {
let _ = write!(out, "\\U+{u:04X}");
}
}
}
}
}
/// `c` in the code page `d`, if it has it, unless it is a double-byte character whose trail byte
/// is 0x5C and an escape's `U+` or `M+` follows (`before_escape`): see [`push_string`].
fn code_page_bytes(c: char, d: codepage::Decoder, before_escape: bool, out: &mut Vec<u8>) -> bool {
let start = out.len();
if !codepage::encode_char(c, d, out) {
return false;
}
if before_escape && out.len() - start > 1 && out.last() == Some(&b'\\') {
out.truncate(start);
return false;
}
true
}
/// A section: `0 SECTION / 2 NAME ... 0 ENDSEC`.
#[derive(Clone, Debug, Default)]
pub struct Section {
pub name: String,
pub tags: Vec<Tag>,
}
/// Split a tag stream into sections.
pub fn sections(tags: &[Tag]) -> Vec<Section> {
section_slices(tags).into_iter().map(|s| Section { name: s.name.to_string(), tags: s.tags.to_vec() }).collect()
}
/// A section borrowed from the tag stream it was split from: [`Section`] without the copy.
#[derive(Clone, Copy, Debug)]
pub struct SectionRef<'a> {
pub name: &'a str,
pub tags: &'a [Tag],
}
/// [`sections`] without copying the tags: each section is a slice of `tags`. A large file's tags
/// take gigabytes (40 bytes a tag, 25 million tags in a 0.5 GB DXF), so a reader that holds the
/// stream while it reads the sections should take them this way.
pub fn section_slices(tags: &[Tag]) -> Vec<SectionRef<'_>> {
let is = |t: &Tag, word: &str| t.code == 0 && t.str_ref().is_some_and(|s| s.trim_end() == word);
let mut out = Vec::new();
let mut i = 0;
while i < tags.len() {
if tags.get(i).is_some_and(|t| is(t, "SECTION")) {
let name = tags.get(i + 1).filter(|t| t.code == 2).and_then(Tag::str_ref).unwrap_or_default().trim_end();
let start = i + 2;
let mut j = start;
// A section cut short has no ENDSEC: the EOF is not one of its records.
while j < tags.len() && !tags.get(j).is_some_and(|t| is(t, "ENDSEC") || is(t, "EOF")) {
j += 1;
}
out.push(SectionRef { name, tags: tags.get(start..j).unwrap_or(&[]) });
i = j + 1;
} else {
i += 1;
}
}
out
}
/// Split a run of tags into records starting at each code-0 tag: (type, tags after the 0).
pub fn records(tags: &[Tag]) -> Vec<(String, Vec<Tag>)> {
record_slices(tags).into_iter().map(|(kind, tags)| (kind, tags.to_vec())).collect()
}
/// [`records`] without copying the tags: each record's tags are a slice of `tags`.
pub fn record_slices(tags: &[Tag]) -> Vec<(String, &[Tag])> {
let starts: Vec<usize> = tags.iter().enumerate().filter(|(_, t)| t.code == 0).map(|(i, _)| i).collect();
starts
.iter()
.enumerate()
.map(|(n, &i)| {
let end = starts.get(n + 1).copied().unwrap_or(tags.len());
(tags.get(i).map(Tag::str).unwrap_or_default(), tags.get(i + 1..end).unwrap_or(&[]))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// The writer's own code and real formatting gives the text `format!` gave.
#[test]
fn fast_code_and_real_formatting_match_format() {
for code in [-1_000_000, -10, -1, 0, 1, 9, 10, 42, 99, 100, 330, 999, 1000, 1071, 9999, 10_000, 65_535, 1_000_000] {
let mut s = Vec::new();
push_code(&mut s, code);
assert_eq!(String::from_utf8(s).unwrap(), format!("{code:>3}\r\n"), "{code}");
}
let old = |v: f64| {
if !v.is_finite() {
return "0.0".to_string();
}
let a = v.abs();
if a != 0.0 && !(1e-10..1e16).contains(&a) {
let e = format!("{v:E}");
let (m, x) = e.split_once('E').unwrap_or((&e, "0"));
let x: i32 = x.parse().unwrap_or(0);
let m = if m.contains('.') { m.to_string() } else { format!("{m}.0") };
return format!("{m}E{}{:02}", if x < 0 { '-' } else { '+' }, x.abs());
}
let s = format!("{v}");
if s.contains('.') { s } else { format!("{s}.0") }
};
for v in [
0.0,
-0.0,
1.0,
-1.0,
0.1,
1.5e-7,
123456789.0,
1e15,
1e16,
1e21,
1e300,
-1e-300,
1e-10,
9e-11,
f64::MAX,
f64::MIN_POSITIVE,
f64::NAN,
f64::INFINITY,
] {
assert_eq!(format_real(v), old(v), "{v}");
}
let tags = [
Tag::f(10, 2.0),
Tag::i(70, -3),
Tag::new(290, Value::Bool(true)),
Tag::s(1, "a\r\nb"),
Tag::s(1, "é"),
Tag::s(1, "plain"),
Tag::s(1, "a\\U+0041"),
];
assert_eq!(
write_ascii_as(&tags, true),
" 10\r\n2.0\r\n 70\r\n-3\r\n290\r\n1\r\n 1\r\na b\r\n 1\r\né\r\n 1\r\nplain\r\n 1\r\na\\U+005CU+0041\r\n"
);
assert_eq!(write_ascii_as(&tags, false).lines().nth(9), Some("\\U+00E9"));
// A code page that writes an ASCII character another way still does.
let yen = [Tag::s(1, "a\\b~")];
assert_eq!(write_ascii_in_page(&yen, "ANSI_932"), ascii_slow(&yen, "ANSI_932"));
}
/// What the writer gave before the fast path: every string through `push_string`.
fn ascii_slow(tags: &[Tag], code_page: &str) -> Vec<u8> {
let mut out = Vec::new();
for t in tags {
let _ = write!(out, "{:>3}\r\n", t.code);
push_string(&t.str(), Strings::Escaped(codepage::encoder(code_page)), &mut out);
out.extend_from_slice(b"\r\n");
}
out
}
#[test]
fn literal_escape_text_roundtrips() {
for utf8 in [false, true] {
for text in ["\\U+00E9", "\\M+183A4F", "a\\nb \\P", "\\\\U+00E9", "\\U+005C"] {
let tags = vec![Tag::s(8, text)];
let back = parse_ascii(&write_ascii_as(&tags, utf8)).unwrap();
assert_eq!(back, tags, "utf8={utf8} {text}");
let back = parse_binary(&write_binary(&tags, utf8)).unwrap();
assert_eq!(back[1..], tags[..], "binary utf8={utf8} {text}");
}
}
}
#[test]
fn blank_lines_do_not_consume_group_code_limit() {
let tags = parse_with_limit(b"0\nEOF\n\n\n", 1).expect("one group code should fit a limit of one");
assert_eq!(tags, vec![Tag::s(0, "EOF")]);
}
#[test]
fn ascii_roundtrip() {
let tags = vec![
Tag::s(0, "SECTION"),
Tag::s(2, "ENTITIES"),
Tag::s(0, "LINE"),
Tag::s(8, "0"),
Tag::f(10, 1.5),
Tag::f(20, 2.0),
Tag::i(62, 1),
Tag::s(0, "ENDSEC"),
Tag::s(0, "EOF"),
];
let text = write_ascii(&tags);
let back = parse(text.as_bytes()).unwrap();
assert_eq!(back, tags);
let secs = sections(&back);
assert_eq!(secs.len(), 1);
assert_eq!(secs[0].name, "ENTITIES");
let recs = records(&secs[0].tags);
assert_eq!(recs[0].0, "LINE");
assert_eq!(recs[0].1.len(), 4);
}
#[test]
fn binary_parse() {
let mut b = BINARY_SENTINEL.to_vec();
let push_str = |b: &mut Vec<u8>, code: i16, s: &str| {
b.extend_from_slice(&code.to_le_bytes());
b.extend_from_slice(s.as_bytes());
b.push(0);
};
push_str(&mut b, 0, "LINE");
b.extend_from_slice(&10i16.to_le_bytes());
b.extend_from_slice(&3.25f64.to_le_bytes());
b.extend_from_slice(&62i16.to_le_bytes());
b.extend_from_slice(&5i16.to_le_bytes());
push_str(&mut b, 0, "EOF");
let tags = parse(&b).unwrap();
assert_eq!(tags[1], Tag::f(10, 3.25));
assert_eq!(tags[2], Tag::i(62, 5));
}
/// A pre-R13 binary body: 1-byte group codes, 255 escaping a 2-byte code (1001 here).
fn r12_binary() -> Vec<u8> {
let mut b = BINARY_SENTINEL.to_vec();
let push_str = |b: &mut Vec<u8>, code: u8, s: &str| {
b.push(code);
b.extend_from_slice(s.as_bytes());
b.push(0);
};
push_str(&mut b, 0, "SECTION");
push_str(&mut b, 2, "ENTITIES");
push_str(&mut b, 0, "LINE");
push_str(&mut b, 8, "RAILROAD");
b.push(10);
b.extend_from_slice(&3.25f64.to_le_bytes());
b.push(62);
b.extend_from_slice(&5i16.to_le_bytes());
b.push(255);
b.extend_from_slice(&1001i16.to_le_bytes());
b.extend_from_slice(b"APP\0");
push_str(&mut b, 0, "ENDSEC");
push_str(&mut b, 0, "EOF");
b
}
#[test]
fn r12_binary_parse() {
let tags = parse(&r12_binary()).unwrap();
assert_eq!(tags[0], Tag::s(0, "SECTION"));
assert_eq!(tags[3], Tag::s(8, "RAILROAD"));
assert_eq!(tags[4], Tag::f(10, 3.25));
assert_eq!(tags[5], Tag::i(62, 5));
assert_eq!(tags[6], Tag::s(1001, "APP"));
assert_eq!(tags.last(), Some(&Tag::s(0, "EOF")));
}
#[test]
fn r13_binary_code_255_is_ordinary() {
let mut b = BINARY_SENTINEL.to_vec();
for (code, s) in [(0i16, "LINE"), (255, "x"), (0, "EOF")] {
b.extend_from_slice(&code.to_le_bytes());
b.extend_from_slice(s.as_bytes());
b.push(0);
}
let tags = parse(&b).unwrap();
assert_eq!(tags.get(1).map(|t| t.code), Some(255));
assert_eq!(tags.len(), 3);
}
#[test]
fn binary_layout_detection() {
assert!(legacy_binary_layout(b"\0SECTION\0"));
assert!(legacy_binary_layout(&[255, 0xE7, 0x03]));
assert!(!legacy_binary_layout(b"\0\0SECTION\0"));
assert!(!legacy_binary_layout(&[0xE7, 0x03, b'x', 0]));
assert!(!legacy_binary_layout(b""));
assert!(!legacy_binary_layout(&[0]));
// R13+ code 255 with the string "x": not the escape
assert!(!legacy_binary_layout(&[255, 0, b'x', 0]));
assert!(!legacy_binary_layout(&[255, 0xE7]));
}
#[test]
fn truncated_r12_binary_never_panics() {
let b = r12_binary();
for n in BINARY_SENTINEL.len()..b.len() {
let _ = parse(b.get(..n).unwrap_or(&[]));
}
let mut cut = BINARY_SENTINEL.to_vec();
cut.extend_from_slice(&[0, b'S', 0, 255, 0x10]);
assert!(parse(&cut).is_err());
}
#[test]
fn borrowed_sections_and_records_match_the_copies() {
let text = "999\nstray\n 0\nSECTION\n 2\nHEADER\n 9\n$ACADVER\n 1\nAC1015\n 0\nENDSEC\n 0\nSECTION\n 2\nENTITIES\n 5\n1A\n 0\nLINE\n 8\n0\n 10\n1.5\n 0\nCIRCLE\n 40\n2.0\n 0\nENDSEC\n 0\nEOF\n";
let tags = parse(text.as_bytes()).unwrap();
let (copies, slices) = (sections(&tags), section_slices(&tags));
assert_eq!(slices.iter().map(|s| s.name).collect::<Vec<_>>(), ["HEADER", "ENTITIES"]);
assert_eq!(copies.len(), slices.len());
for (c, s) in copies.iter().zip(&slices) {
assert_eq!(c.name, s.name);
assert_eq!(c.tags.as_slice(), s.tags);
let (rc, rs) = (records(&c.tags), record_slices(s.tags));
assert_eq!(rc.len(), rs.len());
for ((kc, tc), (ks, ts)) in rc.iter().zip(&rs) {
assert_eq!((kc, tc.as_slice()), (ks, *ts));
}
}
// Tags before the first record start none, as in `records`.
assert_eq!(record_slices(slices[1].tags).iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>(), ["LINE", "CIRCLE"]);
assert_eq!(slices[1].tags[0].str_ref(), Some("1A"));
assert_eq!(Tag::f(10, 1.5).str_ref(), None);
}
#[test]
fn huge_and_tiny_reals_in_exponent_form() {
// Plain digits would run to hundreds of characters; the value reads back exactly.
assert_eq!(format_real(1e20), "1.0E+20");
assert_eq!(format_real(-2.5e-12), "-2.5E-12");
assert_eq!(format_real(0.5), "0.5");
assert_eq!(format_real(12.0), "12.0");
for v in [f64::MAX, -f64::MAX, 1e308, f64::MIN_POSITIVE, 5e-324, 1.2345678901234567e16] {
let s = format_real(v);
assert!(s.len() < 32, "{s}");
assert_eq!(s.parse::<f64>().ok(), Some(v), "{s}");
// and through the writer and the reader
let back = parse(write_ascii(&[Tag::f(10, v), Tag::s(0, "EOF")]).as_bytes()).unwrap();
assert_eq!(back[0], Tag::f(10, v));
}
}
#[test]
fn long_strings_split_into_250_character_groups() {
assert!(string_groups("").is_empty());
assert_eq!(string_groups("abc"), ["abc"]);
let s = "é".repeat(251);
let g = string_groups(&s);
assert_eq!(g.iter().map(|x| x.chars().count()).collect::<Vec<_>>(), [250, 1]);
assert_eq!(g.concat(), s);
assert_eq!(string_groups(&"x".repeat(500)).len(), 2);
}
#[test]
fn malformed_inputs() {
assert!(parse(b"").is_err());
assert!(parse(b"abc\nxyz\n").is_err());
assert!(parse(b"0\n").is_err());
let mut b = BINARY_SENTINEL.to_vec();
b.extend_from_slice(&10i16.to_le_bytes());
b.extend_from_slice(&[1, 2, 3]);
assert!(parse(&b).is_err());
// Bad numbers become 0 rather than failing.
let t = parse(b"10\nnotanumber\n0\nEOF\n").unwrap();
assert!(t[0].f64().abs() < f64::EPSILON);
}
#[test]
fn unicode_escapes_and_latin1() {
let t = parse(b"1\nCaf\\U+00E9\n0\nEOF\n").unwrap();
assert_eq!(t[0].str(), "Café");
let t = parse(b"1\nCaf\xe9\n0\nEOF\n").unwrap();
assert_eq!(t[0].str(), "Café");
}
#[test]
fn surrogate_pair_escapes() {
let t = parse(b"1\n\\U+D83D\\U+DE00 \\u+56fe\\U+53f7\n0\nEOF\n").unwrap();
assert_eq!(t[0].str(), "😀 \\u+56fe号");
}
#[test]
fn hostile_escapes_stay_literal() {
for s in [
"\\U+",
"\\U",
"\\",
"a\\U+ZZZZ",
"\\U+12",
"\\U+D800",
"\\U+DC00",
"\\U+D800\\U+0041",
"\\U+D83D\\U+DE",
"\\U++123",
"\\U+-123",
"x\\U+12\u{e9}4",
] {
let t = parse(format!("1\n{s}\n0\nEOF\n").as_bytes()).unwrap();
let expect = if s == "\\U+D800\\U+0041" { "\\U+D800A" } else { s };
assert_eq!(t[0].str(), expect, "{s:?}");
}
}
#[test]
fn writes_non_ascii_as_escapes() {
let tags = vec![Tag::s(1, "图号 İø 😀\r\nend"), Tag::s(1000, "Ø"), Tag::s(0, "EOF")];
let text = write_ascii(&tags);
assert!(text.is_ascii());
assert!(text.contains("\\U+56FE\\U+53F7 \\U+00C4\\U+00B0\\U+00F8 \\U+D83D\\U+DE00 end\r\n"));
let back = parse(text.as_bytes()).unwrap();
assert_eq!(back[0].str(), "图号 İø 😀 end");
assert_eq!(back[1].str(), "Ø");
}
#[test]
fn binary_strings_unescape() {
let mut b = BINARY_SENTINEL.to_vec();
b.extend_from_slice(&1i16.to_le_bytes());
b.extend_from_slice(b"\\U+56FE\\U+53F7\0");
b.extend_from_slice(&0i16.to_le_bytes());
b.extend_from_slice(b"EOF\0");
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.
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}");
}
}