mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 20:40:30 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
664837df89 | ||
|
|
2c953e38a3 |
Generated
+13
-13
@@ -695,15 +695,6 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-dxf"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-engine"
|
||||
version = "0.17.0"
|
||||
@@ -711,12 +702,12 @@ dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-constraints",
|
||||
"cadcraft-doc",
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-io",
|
||||
"cadcraft-render",
|
||||
"craft-docpath",
|
||||
"craft-dxf",
|
||||
"libc",
|
||||
"log",
|
||||
"rstar",
|
||||
@@ -729,8 +720,8 @@ dependencies = [
|
||||
name = "cadcraft-fonts"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-geom",
|
||||
"craft-dxf",
|
||||
"encoding_rs",
|
||||
"harfrust 0.14.0",
|
||||
"icu_segmenter",
|
||||
@@ -753,10 +744,10 @@ dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-doc",
|
||||
"cadcraft-dwg",
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-render",
|
||||
"craft-dxf",
|
||||
"craft-pdf",
|
||||
"image",
|
||||
"miniz_oxide 0.9.1",
|
||||
@@ -776,7 +767,6 @@ dependencies = [
|
||||
"cadcraft-render",
|
||||
"clap",
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"serde_json",
|
||||
"tiny-skia",
|
||||
@@ -1152,6 +1142,16 @@ name = "craft-docpath"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-dxf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=006e4df225fca57e95ead0fb1f91920900798a99#006e4df225fca57e95ead0fb1f91920900798a99"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ repository = "https://github.com/storytold/cadcraft"
|
||||
[workspace.dependencies]
|
||||
# internal
|
||||
cadcraft-geom = { path = "crates/geom" }
|
||||
cadcraft-dxf = { path = "crates/dxf" }
|
||||
# DXF tags: the shared reader and writer (moved from crates/dxf; craft-libs#791).
|
||||
craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "006e4df225fca57e95ead0fb1f91920900798a99" }
|
||||
cadcraft-dwg = { path = "crates/dwg" }
|
||||
cadcraft-color = { path = "crates/color" }
|
||||
cadcraft-doc = { path = "crates/doc" }
|
||||
|
||||
@@ -148,20 +148,7 @@ pub fn rotate(dir: &Path) -> std::io::Result<(PathBuf, File)> {
|
||||
/// One formatted log line: `<timestamp> <LEVEL> [<thread>] <target>: <message>\n`.
|
||||
pub fn format_line(ts: &str, level: log::Level, thread: &str, target: &str, message: &str) -> String {
|
||||
let message = message.strip_suffix('\n').unwrap_or(message);
|
||||
// A message can carry text from a file (a code page name, a path), so it must not be able to
|
||||
// start what looks like another record: control characters other than tab and line feed are
|
||||
// shown escaped, and every continuation line is indented, so only this function's own
|
||||
// timestamp is ever at the start of a line.
|
||||
let mut safe = String::with_capacity(message.len());
|
||||
for c in message.chars() {
|
||||
match c {
|
||||
'\n' => safe.push_str("\n "),
|
||||
'\t' => safe.push('\t'),
|
||||
c if c.is_control() => safe.extend(c.escape_default()),
|
||||
c => safe.push(c),
|
||||
}
|
||||
}
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {safe}\n")
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {message}\n")
|
||||
}
|
||||
|
||||
/// Where formatted lines go besides standard error: kept in memory until a file is attached,
|
||||
@@ -454,14 +441,8 @@ mod tests {
|
||||
format_line("2026-10-08T07:59:17.728Z", log::Level::Warn, "main", "cadcraft", "app icon: invalid PNG signature"),
|
||||
"2026-10-08T07:59:17.728Z WARN [main] cadcraft: app icon: invalid PNG signature\n"
|
||||
);
|
||||
// A multi-line message keeps its lines, indented so they can't pass for records; the
|
||||
// record still ends in exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\n b\n"));
|
||||
// Control characters in a message (from a file) can't forge a record.
|
||||
let forged = format_line("t", log::Level::Warn, "w", "x", "page\r\n2026-01-01T00:00:00Z ERROR [main] x: fake\u{1b}[0m");
|
||||
assert_eq!(forged.lines().count(), 2);
|
||||
assert!(forged.lines().skip(1).all(|l| l.starts_with(" ")));
|
||||
assert!(!forged.contains('\r') && !forged.contains('\u{1b}'));
|
||||
// A multi-line message keeps its lines; the record still ends in exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\nb\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -138,9 +138,7 @@ pub fn dof(d: &Drawing, handles: &[Handle]) -> Result<DofReport, SolveError> {
|
||||
let cons: Vec<&Constraint> = d.constraints.iter().take(MAX_CONSTRAINTS).collect();
|
||||
let mut prep = Prep::new(crate::model::MAX_RIGID_WORK);
|
||||
let (comps, _) = build_with(d, &cons, &SolveOptions::default(), &mut prep)?;
|
||||
// The selection as a set, built once: a scan of the list per slot multiplied the two sizes.
|
||||
let selected: HashSet<Handle> = handles.iter().copied().collect();
|
||||
let want = |h: Handle| handles.is_empty() || selected.contains(&h);
|
||||
let want = |h: Handle| handles.is_empty() || handles.contains(&h);
|
||||
let mut objects = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
let mut total = Some(0usize);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "cadcraft-dxf"
|
||||
description = "Standalone DXF tag reader/writer (ASCII and binary, pre-2007 code pages), from the published DXF reference"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
@@ -1,398 +0,0 @@
|
||||
//! Text decoding for DXF files.
|
||||
//!
|
||||
//! R2007 (AC1021) and later DXF files are UTF-8. Older ones store text in the code page named by
|
||||
//! the `$DWGCODEPAGE` header variable (`ANSI_1252`, `ANSI_932`, `DOS850`, …); characters outside
|
||||
//! it are written as `\U+XXXX` (any character) or `\M+nXXXX` (a double-byte character of an East
|
||||
//! Asian code page) escapes, which [`crate::parse`] decodes after the bytes.
|
||||
//!
|
||||
//! The Windows, ISO and East Asian code pages are decoded with `encoding_rs`; DOS 437, 850 and
|
||||
//! 852 with the tables below (the upper halves of the IBM PC code pages; the lower halves are
|
||||
//! ASCII). Decoding never fails: a byte sequence that is invalid in the code page (a truncated
|
||||
//! double-byte character, an unassigned byte) becomes U+FFFD and the bytes after it decode
|
||||
//! normally, so line ends and the group structure survive.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
|
||||
/// How the bytes of a code page become text.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum Decoder {
|
||||
Utf8,
|
||||
Encoding(&'static Encoding),
|
||||
/// Single-byte DOS code page: ASCII, then these 128 characters for bytes 0x80–0xFF.
|
||||
Table(&'static [char; 128]),
|
||||
}
|
||||
|
||||
/// The decoder for a `$DWGCODEPAGE` value (case-insensitive), or `None` when CADCraft doesn't
|
||||
/// know the code page.
|
||||
pub(crate) fn decoder(code_page: &str) -> Option<Decoder> {
|
||||
use encoding_rs as e;
|
||||
let name = code_page.trim().to_ascii_uppercase().replace(['-', ' '], "_");
|
||||
let enc = match name.as_str() {
|
||||
"UTF8" | "UTF_8" | "UNICODE" => return Some(Decoder::Utf8),
|
||||
"ASCII" | "ANSI_1252" | "ISO8859_1" | "ISO_8859_1" => e::WINDOWS_1252,
|
||||
"ANSI_874" => e::WINDOWS_874,
|
||||
"ANSI_932" | "DOS932" | "SHIFT_JIS" => e::SHIFT_JIS,
|
||||
"ANSI_936" | "GB2312" | "GBK" => e::GBK,
|
||||
"ANSI_949" | "KSC5601" | "KOREAN" => e::EUC_KR,
|
||||
"ANSI_950" | "BIG5" => e::BIG5,
|
||||
"ANSI_1250" => e::WINDOWS_1250,
|
||||
"ANSI_1251" => e::WINDOWS_1251,
|
||||
"ANSI_1253" => e::WINDOWS_1253,
|
||||
"ANSI_1254" | "ISO8859_9" | "ISO_8859_9" => e::WINDOWS_1254,
|
||||
"ANSI_1255" => e::WINDOWS_1255,
|
||||
"ANSI_1256" => e::WINDOWS_1256,
|
||||
"ANSI_1257" => e::WINDOWS_1257,
|
||||
"ANSI_1258" => e::WINDOWS_1258,
|
||||
"ISO8859_2" | "ISO_8859_2" => e::ISO_8859_2,
|
||||
"ISO8859_3" | "ISO_8859_3" => e::ISO_8859_3,
|
||||
"ISO8859_4" | "ISO_8859_4" => e::ISO_8859_4,
|
||||
"ISO8859_5" | "ISO_8859_5" => e::ISO_8859_5,
|
||||
"ISO8859_6" | "ISO_8859_6" => e::ISO_8859_6,
|
||||
"ISO8859_7" | "ISO_8859_7" => e::ISO_8859_7,
|
||||
"ISO8859_8" | "ISO_8859_8" => e::ISO_8859_8,
|
||||
"DOS866" => e::IBM866,
|
||||
"MACINTOSH" | "MAC_ROMAN" => e::MACINTOSH,
|
||||
"DOS437" => return Some(Decoder::Table(&DOS437)),
|
||||
"DOS850" => return Some(Decoder::Table(&DOS850)),
|
||||
"DOS852" => return Some(Decoder::Table(&DOS852)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Decoder::Encoding(enc))
|
||||
}
|
||||
|
||||
/// Decode `bytes` from the code page `code_page` names (a `$DWGCODEPAGE` value such as
|
||||
/// `ANSI_1251`), or `None` when CADCraft doesn't know that code page. Invalid sequences become
|
||||
/// U+FFFD; escapes (`\U+XXXX`, `\M+nXXXX`) are left as they are.
|
||||
pub fn decode_code_page(bytes: &[u8], code_page: &str) -> Option<String> {
|
||||
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<Decoder> {
|
||||
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<u8>) -> bool {
|
||||
let mut buf = [0; 4];
|
||||
match d {
|
||||
// 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,
|
||||
// CR or LF would break the group, so only all-high-bit sequences or proper double-byte
|
||||
// characters are kept (their trail byte may be ASCII, as in Shift-JIS).
|
||||
if unmappable || bytes.is_empty() || !bytes.first().is_some_and(|b| *b >= 0x80) || bytes.iter().any(|b| matches!(b, 0 | b'\r' | b'\n')) {
|
||||
return false;
|
||||
}
|
||||
out.extend_from_slice(&bytes);
|
||||
}
|
||||
Decoder::Table(t) => {
|
||||
let Some(i) = t.iter().position(|&x| x == c).and_then(|i| u8::try_from(i).ok()) else { return false };
|
||||
out.push(0x80 | i);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Decode bytes with a decoder. UTF-8 that isn't valid falls back to Windows-1252.
|
||||
pub(crate) fn decode(bytes: &[u8], d: Decoder) -> Cow<'_, str> {
|
||||
match d {
|
||||
Decoder::Utf8 => match std::str::from_utf8(bytes) {
|
||||
Ok(s) => Cow::Borrowed(s),
|
||||
Err(_) => encoding_rs::WINDOWS_1252.decode_without_bom_handling(bytes).0,
|
||||
},
|
||||
Decoder::Encoding(e) => e.decode_without_bom_handling(bytes).0,
|
||||
Decoder::Table(t) => match std::str::from_utf8(bytes) {
|
||||
Ok(s) if s.is_ascii() => Cow::Borrowed(s),
|
||||
_ => Cow::Owned(
|
||||
bytes
|
||||
.iter()
|
||||
.map(|&b| if b.is_ascii() { char::from(b) } else { t.get(usize::from(b & 0x7F)).copied().unwrap_or('\u{FFFD}') })
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The `$DWGCODEPAGE` value of an ASCII DXF file: the line two lines after a `$DWGCODEPAGE`
|
||||
/// 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' || b == b'\r').map(<[u8]>::trim_ascii).filter(|l| !l.is_empty());
|
||||
while let Some(line) = lines.next() {
|
||||
if line == b"ENDSEC" {
|
||||
return None;
|
||||
}
|
||||
if line.eq_ignore_ascii_case(b"$DWGCODEPAGE") {
|
||||
return match (lines.next(), lines.next()) {
|
||||
(Some(b"3"), Some(value)) => Some(value),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
/// True for the multi-byte East Asian code pages (Shift-JIS, GBK, EUC-KR/Wansung, Big5).
|
||||
/// Their short double-byte strings are often also valid UTF-8, so UTF-8 is only trusted for
|
||||
/// a whole file there (see [`decode_ascii_file`] and [`BinaryText`]).
|
||||
fn is_east_asian(self) -> bool {
|
||||
use encoding_rs as e;
|
||||
matches!(self, Decoder::Encoding(x) if x == e::SHIFT_JIS || x == e::GBK || x == e::EUC_KR || x == e::BIG5)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one line of an ASCII 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`, so one stray byte spoils one line, not the whole file. Binary strings don't use
|
||||
/// this rule: a pre-R2007 binary file has no way to escape to UTF-8, so its strings are read
|
||||
/// from the code page (see [`BinaryText::string`]).
|
||||
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. A file that is valid UTF-8 throughout is UTF-8. Otherwise, for
|
||||
/// a single-byte code page, each line is decoded on its own (see [`decode_text`]): lines that
|
||||
/// aren't valid UTF-8 come 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. For a multi-byte East Asian code page the whole file is decoded from
|
||||
/// that code page, because a short double-byte string (two half-width katakana, many kanji
|
||||
/// pairs) can be valid UTF-8 by accident and must not be misread line by line.
|
||||
pub(crate) fn decode_ascii_file(bytes: &[u8]) -> Cow<'_, str> {
|
||||
if let Ok(s) = std::str::from_utf8(bytes) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
let d = match ascii_code_page(bytes) {
|
||||
Some(name) => {
|
||||
let name = String::from_utf8_lossy(name);
|
||||
decoder(&name).unwrap_or_else(|| {
|
||||
log::warn!("DXF: unknown code page `{name}`; reading the text as ANSI_1252");
|
||||
Decoder::Utf8
|
||||
})
|
||||
}
|
||||
None => Decoder::Utf8,
|
||||
};
|
||||
if d.is_east_asian() {
|
||||
return 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 the header as the tags go by: `$ACADVER`
|
||||
/// (R2007+ strings are UTF-8) and `$DWGCODEPAGE` (the code page of older files). Both come
|
||||
/// first in the HEADER section and are ASCII, so every later string is decoded right.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BinaryText {
|
||||
/// The header variable whose value comes next (after a group 9).
|
||||
var: Option<HeaderVar>,
|
||||
utf8: bool,
|
||||
code_page: Option<Decoder>,
|
||||
/// Decode every string of an East Asian code page file from the code page, never as UTF-8
|
||||
/// (the second pass of [`BinaryText::needs_code_page_pass`]).
|
||||
code_page_only: bool,
|
||||
/// Non-ASCII strings seen under an East Asian code page: valid UTF-8 / not valid UTF-8.
|
||||
ea_utf8: bool,
|
||||
ea_other: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum HeaderVar {
|
||||
AcadVer,
|
||||
CodePage,
|
||||
}
|
||||
|
||||
impl BinaryText {
|
||||
/// The state for the second pass over a file whose strings are a mix of valid UTF-8 and
|
||||
/// not: every string of an East Asian code page is decoded from that code page.
|
||||
pub(crate) fn code_page_pass() -> Self {
|
||||
Self { code_page_only: true, ..Self::default() }
|
||||
}
|
||||
|
||||
/// True when the file declared a multi-byte East Asian code page and its non-ASCII strings are
|
||||
/// not all valid UTF-8 although some are: those can be short double-byte strings that look
|
||||
/// like UTF-8 by accident, so the file must be parsed again with [`Self::code_page_pass`]. (A
|
||||
/// file whose strings are all valid UTF-8 is UTF-8; one with none is plainly the code page.)
|
||||
pub(crate) fn needs_code_page_pass(&self) -> bool {
|
||||
!self.code_page_only && self.ea_utf8 && self.ea_other
|
||||
}
|
||||
|
||||
/// 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); a
|
||||
/// pre-R2007 file's strings are in its code page, which a binary DXF cannot escape to, so
|
||||
/// they are read from the code page even when their bytes are valid UTF-8 by accident —
|
||||
/// except under an East Asian code page, where a file of all-valid-UTF-8 strings is taken
|
||||
/// as UTF-8 (see [`Self::needs_code_page_pass`]).
|
||||
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 = if d.is_east_asian() && !self.code_page_only {
|
||||
if !raw.is_ascii() {
|
||||
if std::str::from_utf8(raw).is_ok() {
|
||||
self.ea_utf8 = true;
|
||||
} else {
|
||||
self.ea_other = true;
|
||||
}
|
||||
}
|
||||
decode_text(raw, d).into_owned()
|
||||
} else {
|
||||
decode(raw, d).into_owned()
|
||||
};
|
||||
match (code, self.var.take()) {
|
||||
(9, _) => {
|
||||
self.var = match s.trim() {
|
||||
"$ACADVER" => Some(HeaderVar::AcadVer),
|
||||
"$DWGCODEPAGE" => Some(HeaderVar::CodePage),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(1, Some(HeaderVar::AcadVer)) => self.utf8 = is_utf8_version(&s),
|
||||
(3, Some(HeaderVar::CodePage)) => {
|
||||
self.code_page = decoder(&s);
|
||||
if self.code_page.is_none() {
|
||||
log::warn!("DXF: unknown code page `{}`; reading the text as ANSI_1252", s.trim());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// True for an `$ACADVER` of R2007 (AC1021) or later, whose text is UTF-8.
|
||||
fn is_utf8_version(acadver: &str) -> bool {
|
||||
acadver.trim().strip_prefix("AC").and_then(|n| n.parse::<u32>().ok()).is_some_and(|n| n >= 1021)
|
||||
}
|
||||
|
||||
/// The double-byte code page of a `\M+n` escape: 1 Japanese (Shift-JIS), 2 Traditional Chinese
|
||||
/// (Big5), 3 Korean (Wansung), 5 Simplified Chinese (GB 2312). 4 (Korean Johab) is not
|
||||
/// supported, so its escapes stay literal.
|
||||
pub(crate) fn mif_encoding(n: char) -> Option<&'static Encoding> {
|
||||
match n {
|
||||
'1' => Some(encoding_rs::SHIFT_JIS),
|
||||
'2' => Some(encoding_rs::BIG5),
|
||||
'3' => Some(encoding_rs::EUC_KR),
|
||||
'5' => Some(encoding_rs::GBK),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Generated from the IBM PC code page definitions (bytes 0x80–0xFF in order).
|
||||
const DOS437: [char; 128] = [
|
||||
'\u{00C7}', '\u{00FC}', '\u{00E9}', '\u{00E2}', '\u{00E4}', '\u{00E0}', '\u{00E5}', '\u{00E7}', '\u{00EA}', '\u{00EB}', '\u{00E8}', '\u{00EF}',
|
||||
'\u{00EE}', '\u{00EC}', '\u{00C4}', '\u{00C5}', '\u{00C9}', '\u{00E6}', '\u{00C6}', '\u{00F4}', '\u{00F6}', '\u{00F2}', '\u{00FB}', '\u{00F9}',
|
||||
'\u{00FF}', '\u{00D6}', '\u{00DC}', '\u{00A2}', '\u{00A3}', '\u{00A5}', '\u{20A7}', '\u{0192}', '\u{00E1}', '\u{00ED}', '\u{00F3}', '\u{00FA}',
|
||||
'\u{00F1}', '\u{00D1}', '\u{00AA}', '\u{00BA}', '\u{00BF}', '\u{2310}', '\u{00AC}', '\u{00BD}', '\u{00BC}', '\u{00A1}', '\u{00AB}', '\u{00BB}',
|
||||
'\u{2591}', '\u{2592}', '\u{2593}', '\u{2502}', '\u{2524}', '\u{2561}', '\u{2562}', '\u{2556}', '\u{2555}', '\u{2563}', '\u{2551}', '\u{2557}',
|
||||
'\u{255D}', '\u{255C}', '\u{255B}', '\u{2510}', '\u{2514}', '\u{2534}', '\u{252C}', '\u{251C}', '\u{2500}', '\u{253C}', '\u{255E}', '\u{255F}',
|
||||
'\u{255A}', '\u{2554}', '\u{2569}', '\u{2566}', '\u{2560}', '\u{2550}', '\u{256C}', '\u{2567}', '\u{2568}', '\u{2564}', '\u{2565}', '\u{2559}',
|
||||
'\u{2558}', '\u{2552}', '\u{2553}', '\u{256B}', '\u{256A}', '\u{2518}', '\u{250C}', '\u{2588}', '\u{2584}', '\u{258C}', '\u{2590}', '\u{2580}',
|
||||
'\u{03B1}', '\u{00DF}', '\u{0393}', '\u{03C0}', '\u{03A3}', '\u{03C3}', '\u{00B5}', '\u{03C4}', '\u{03A6}', '\u{0398}', '\u{03A9}', '\u{03B4}',
|
||||
'\u{221E}', '\u{03C6}', '\u{03B5}', '\u{2229}', '\u{2261}', '\u{00B1}', '\u{2265}', '\u{2264}', '\u{2320}', '\u{2321}', '\u{00F7}', '\u{2248}',
|
||||
'\u{00B0}', '\u{2219}', '\u{00B7}', '\u{221A}', '\u{207F}', '\u{00B2}', '\u{25A0}', '\u{00A0}',
|
||||
];
|
||||
const DOS850: [char; 128] = [
|
||||
'\u{00C7}', '\u{00FC}', '\u{00E9}', '\u{00E2}', '\u{00E4}', '\u{00E0}', '\u{00E5}', '\u{00E7}', '\u{00EA}', '\u{00EB}', '\u{00E8}', '\u{00EF}',
|
||||
'\u{00EE}', '\u{00EC}', '\u{00C4}', '\u{00C5}', '\u{00C9}', '\u{00E6}', '\u{00C6}', '\u{00F4}', '\u{00F6}', '\u{00F2}', '\u{00FB}', '\u{00F9}',
|
||||
'\u{00FF}', '\u{00D6}', '\u{00DC}', '\u{00F8}', '\u{00A3}', '\u{00D8}', '\u{00D7}', '\u{0192}', '\u{00E1}', '\u{00ED}', '\u{00F3}', '\u{00FA}',
|
||||
'\u{00F1}', '\u{00D1}', '\u{00AA}', '\u{00BA}', '\u{00BF}', '\u{00AE}', '\u{00AC}', '\u{00BD}', '\u{00BC}', '\u{00A1}', '\u{00AB}', '\u{00BB}',
|
||||
'\u{2591}', '\u{2592}', '\u{2593}', '\u{2502}', '\u{2524}', '\u{00C1}', '\u{00C2}', '\u{00C0}', '\u{00A9}', '\u{2563}', '\u{2551}', '\u{2557}',
|
||||
'\u{255D}', '\u{00A2}', '\u{00A5}', '\u{2510}', '\u{2514}', '\u{2534}', '\u{252C}', '\u{251C}', '\u{2500}', '\u{253C}', '\u{00E3}', '\u{00C3}',
|
||||
'\u{255A}', '\u{2554}', '\u{2569}', '\u{2566}', '\u{2560}', '\u{2550}', '\u{256C}', '\u{00A4}', '\u{00F0}', '\u{00D0}', '\u{00CA}', '\u{00CB}',
|
||||
'\u{00C8}', '\u{0131}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{2518}', '\u{250C}', '\u{2588}', '\u{2584}', '\u{00A6}', '\u{00CC}', '\u{2580}',
|
||||
'\u{00D3}', '\u{00DF}', '\u{00D4}', '\u{00D2}', '\u{00F5}', '\u{00D5}', '\u{00B5}', '\u{00FE}', '\u{00DE}', '\u{00DA}', '\u{00DB}', '\u{00D9}',
|
||||
'\u{00FD}', '\u{00DD}', '\u{00AF}', '\u{00B4}', '\u{00AD}', '\u{00B1}', '\u{2017}', '\u{00BE}', '\u{00B6}', '\u{00A7}', '\u{00F7}', '\u{00B8}',
|
||||
'\u{00B0}', '\u{00A8}', '\u{00B7}', '\u{00B9}', '\u{00B3}', '\u{00B2}', '\u{25A0}', '\u{00A0}',
|
||||
];
|
||||
const DOS852: [char; 128] = [
|
||||
'\u{00C7}', '\u{00FC}', '\u{00E9}', '\u{00E2}', '\u{00E4}', '\u{016F}', '\u{0107}', '\u{00E7}', '\u{0142}', '\u{00EB}', '\u{0150}', '\u{0151}',
|
||||
'\u{00EE}', '\u{0179}', '\u{00C4}', '\u{0106}', '\u{00C9}', '\u{0139}', '\u{013A}', '\u{00F4}', '\u{00F6}', '\u{013D}', '\u{013E}', '\u{015A}',
|
||||
'\u{015B}', '\u{00D6}', '\u{00DC}', '\u{0164}', '\u{0165}', '\u{0141}', '\u{00D7}', '\u{010D}', '\u{00E1}', '\u{00ED}', '\u{00F3}', '\u{00FA}',
|
||||
'\u{0104}', '\u{0105}', '\u{017D}', '\u{017E}', '\u{0118}', '\u{0119}', '\u{00AC}', '\u{017A}', '\u{010C}', '\u{015F}', '\u{00AB}', '\u{00BB}',
|
||||
'\u{2591}', '\u{2592}', '\u{2593}', '\u{2502}', '\u{2524}', '\u{00C1}', '\u{00C2}', '\u{011A}', '\u{015E}', '\u{2563}', '\u{2551}', '\u{2557}',
|
||||
'\u{255D}', '\u{017B}', '\u{017C}', '\u{2510}', '\u{2514}', '\u{2534}', '\u{252C}', '\u{251C}', '\u{2500}', '\u{253C}', '\u{0102}', '\u{0103}',
|
||||
'\u{255A}', '\u{2554}', '\u{2569}', '\u{2566}', '\u{2560}', '\u{2550}', '\u{256C}', '\u{00A4}', '\u{0111}', '\u{0110}', '\u{010E}', '\u{00CB}',
|
||||
'\u{010F}', '\u{0147}', '\u{00CD}', '\u{00CE}', '\u{011B}', '\u{2518}', '\u{250C}', '\u{2588}', '\u{2584}', '\u{0162}', '\u{016E}', '\u{2580}',
|
||||
'\u{00D3}', '\u{00DF}', '\u{00D4}', '\u{0143}', '\u{0144}', '\u{0148}', '\u{0160}', '\u{0161}', '\u{0154}', '\u{00DA}', '\u{0155}', '\u{0170}',
|
||||
'\u{00FD}', '\u{00DD}', '\u{0163}', '\u{00B4}', '\u{00AD}', '\u{02DD}', '\u{02DB}', '\u{02C7}', '\u{02D8}', '\u{00A7}', '\u{00F7}', '\u{00B8}',
|
||||
'\u{00B0}', '\u{00A8}', '\u{02D9}', '\u{0171}', '\u{0158}', '\u{0159}', '\u{25A0}', '\u{00A0}',
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn code_page_names() {
|
||||
let d = |bytes: &[u8], cp: &str| decode_code_page(bytes, cp);
|
||||
assert_eq!(d(b"\xCF\xF0\xE8\xE2\xE5\xF2", "ANSI_1251").as_deref(), Some("Привет"));
|
||||
assert_eq!(d(b"\xCF\xF0\xE8\xE2\xE5\xF2", " ansi_1251 ").as_deref(), Some("Привет"));
|
||||
assert_eq!(d(b"\x90\x7D\x96\xCA", "ANSI_932").as_deref(), Some("図面"));
|
||||
assert_eq!(d(b"\xCD\xBC\xB2\xE3", "GB2312").as_deref(), Some("图层"));
|
||||
assert_eq!(d(b"\xB9\xCF\xBC\x68", "BIG5").as_deref(), Some("圖層"));
|
||||
assert_eq!(d(b"\xB5\xB5\xB8\xE9", "ANSI_949").as_deref(), Some("도면"));
|
||||
assert_eq!(d(b"\xD3\xF7\xDD\xE4\xE9\xEF", "ANSI_1253").as_deref(), Some("Σχέδιο"));
|
||||
assert_eq!(d(b"\xC0\xD2\xBE", "ANSI_874").as_deref(), Some("ภาพ"));
|
||||
assert_eq!(d(b"\x80", "ANSI_1252").as_deref(), Some("€"));
|
||||
// DOS 437 and 850 agree on letters but differ on 0x9B (¢ / ø).
|
||||
assert_eq!(d(b"Gr\x94\xE1e \x9B", "DOS437").as_deref(), Some("Größe ¢"));
|
||||
assert_eq!(d(b"Gr\x94\xE1e \x9B", "dos850").as_deref(), Some("Größe ø"));
|
||||
assert_eq!(d(b"\xBD\xA2\x88w", "DOS852").as_deref(), Some("Żółw"));
|
||||
assert_eq!(d(b"\x91\xAB\xAE\xA9", "DOS866").as_deref(), Some("Слой"));
|
||||
assert_eq!(d("é".as_bytes(), "UTF8").as_deref(), Some("é"));
|
||||
assert_eq!(d(b"x", "ANSI_1361"), None);
|
||||
assert_eq!(d(b"x", "EBCDIC"), None);
|
||||
assert_eq!(d(b"x", ""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_sequences_become_replacement_characters() {
|
||||
// A Shift-JIS lead byte cut off by the line end keeps the line end.
|
||||
assert_eq!(decode_code_page(b"\x90\n1\n", "ANSI_932").as_deref(), Some("\u{FFFD}\n1\n"));
|
||||
assert_eq!(decode_code_page(b"\x90", "ANSI_932").as_deref(), Some("\u{FFFD}"));
|
||||
assert_eq!(decode_code_page(b"\xFF\xFF", "ANSI_936").as_deref(), Some("\u{FFFD}\u{FFFD}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_scan() {
|
||||
let head = b"0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DWGCODEPAGE\n3\nANSI_1251\r\n0\nENDSEC\n";
|
||||
assert_eq!(ascii_code_page(head), Some(&b"ANSI_1251"[..]));
|
||||
// Only the HEADER section counts.
|
||||
assert_eq!(ascii_code_page(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n9\n$DWGCODEPAGE\n3\nANSI_1251\n"), None);
|
||||
// Wrong group code, or the file ends early.
|
||||
assert_eq!(ascii_code_page(b"9\n$DWGCODEPAGE\n1\nANSI_1251\n"), None);
|
||||
assert_eq!(ascii_code_page(b"9\n$DWGCODEPAGE\n3"), None);
|
||||
assert!(is_utf8_version("AC1021") && is_utf8_version(" AC1032 "));
|
||||
assert!(!is_utf8_version("AC1018") && !is_utf8_version("AC1009") && !is_utf8_version("garbage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mif_escapes() {
|
||||
let one = |s: &str| crate::parse(format!("1\n{s}\n0\nEOF\n").as_bytes()).unwrap()[0].str();
|
||||
assert_eq!(one("\\M+5BCFE\\M+1907D\\M+2B9CF\\M+3B5B5!"), "件図圖도!");
|
||||
assert_eq!(one("\\M+5BCFE\\U+0021"), "件!");
|
||||
// Johab, unknown code pages, bad hex, short escapes and unassigned codes stay literal.
|
||||
for s in ["\\M+4ABCD", "\\M+9BCFE", "\\M+5GGGG", "\\M+5BC", "\\M+", "\\M+50041", "\\M+5FFFF"] {
|
||||
assert_eq!(one(s), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,925 +0,0 @@
|
||||
//! 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. Mapping
|
||||
//! tags to a drawing model is left to the caller. Written from Autodesk's published DXF
|
||||
//! Reference; depends on nothing else in the workspace.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::io::Write as _;
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
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 {
|
||||
0..=9 => Kind::Str,
|
||||
10..=59 => Kind::Real,
|
||||
60..=79 => Kind::Int16,
|
||||
90..=99 => Kind::Int32,
|
||||
100..=109 => Kind::Str,
|
||||
110..=149 => Kind::Real,
|
||||
160..=169 => Kind::Int64,
|
||||
170..=179 => Kind::Int16,
|
||||
210..=239 => Kind::Real,
|
||||
270..=289 => Kind::Int16,
|
||||
290..=299 => Kind::Bool,
|
||||
300..=309 => Kind::Str,
|
||||
310..=319 => Kind::Hex,
|
||||
320..=369 => Kind::Str,
|
||||
370..=389 => Kind::Int16,
|
||||
390..=399 => Kind::Str,
|
||||
400..=409 => Kind::Int16,
|
||||
410..=419 => Kind::Str,
|
||||
420..=429 => Kind::Int32,
|
||||
430..=439 => Kind::Str,
|
||||
440..=449 => Kind::Int32,
|
||||
450..=459 => Kind::Int32,
|
||||
460..=469 => Kind::Real,
|
||||
470..=479 => Kind::Str,
|
||||
480..=481 => Kind::Str,
|
||||
999 => Kind::Str,
|
||||
1000..=1003 => Kind::Str,
|
||||
1004 => Kind::Hex,
|
||||
1005..=1009 => Kind::Str,
|
||||
1010..=1059 => Kind::Real,
|
||||
1060..=1070 => Kind::Int16,
|
||||
1071 => Kind::Int32,
|
||||
_ => Kind::Str,
|
||||
}
|
||||
}
|
||||
|
||||
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(Value::Real).unwrap_or(Value::Real(0.0)),
|
||||
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(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(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).
|
||||
pub fn parse(bytes: &[u8]) -> Result<Vec<Tag>> {
|
||||
parse_with_limit(bytes, max_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.
|
||||
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);
|
||||
// Count nonblank group-code/value pairs (LF, CRLF and a lone CR each end a line), stopping at
|
||||
// EOF just as the parser does. Blank lines do not count toward the limit.
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 enc = codepage::mif_encoding(*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()?))?;
|
||||
let bytes = code.to_be_bytes();
|
||||
let text = enc.decode_without_bom_handling_and_without_replacement(&bytes)?;
|
||||
let mut it = text.chars();
|
||||
it.next().filter(|_| it.next().is_none())
|
||||
}
|
||||
|
||||
/// 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 !s.contains("\\U+") && !s.contains("\\M+") {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let mut i = 0;
|
||||
while let Some(&c) = chars.get(i) {
|
||||
if let Some(ch) = mif_escape_at(&chars, i) {
|
||||
out.push(ch);
|
||||
i += 8;
|
||||
continue;
|
||||
}
|
||||
if let Some(unit) = escape_at(&chars, i) {
|
||||
if let Some(ch) = char::from_u32(unit) {
|
||||
out.push(ch);
|
||||
i += 7;
|
||||
continue;
|
||||
}
|
||||
if (0xD800..0xDC00).contains(&unit)
|
||||
&& let Some(low) = escape_at(&chars, i + 7).filter(|u| (0xDC00..0xE000).contains(u))
|
||||
&& let Some(ch) = char::from_u32(0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00))
|
||||
{
|
||||
out.push(ch);
|
||||
i += 14;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(c);
|
||||
i += 1;
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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.
|
||||
#[allow(clippy::while_let_loop)]
|
||||
fn parse_ascii_counted(text: &str, max_tags: usize, expected: usize) -> Result<Vec<Tag>> {
|
||||
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();
|
||||
loop {
|
||||
let Some((ln, code_line)) = lines.next() else { break };
|
||||
let ct = code_line.trim();
|
||||
if ct.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let code: i32 = ct.parse().map_err(|_| DxfError::BadCode { line: ln + 1, text: ct.chars().take(40).collect() })?;
|
||||
let Some((_, val)) = lines.next() else { return Err(DxfError::Eof) };
|
||||
let v = typed(code, val);
|
||||
let v = match v {
|
||||
Value::Str(s) => Value::Str(unescape_unicode(&s)),
|
||||
o => o,
|
||||
};
|
||||
tags.push(Tag { code, value: v });
|
||||
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().trim_end() == "EOF") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if tags.is_empty() {
|
||||
return Err(DxfError::NotDxf);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
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 start = self.p;
|
||||
while self.p < self.b.len() && self.b.get(self.p) != Some(&0) {
|
||||
self.p += 1;
|
||||
}
|
||||
let s = self.b.get(start..self.p).unwrap_or(&[]);
|
||||
if self.p >= self.b.len() {
|
||||
return Err(DxfError::Eof);
|
||||
}
|
||||
self.p += 1;
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary DXF (R13+: 2-byte group codes).
|
||||
pub fn parse_binary(b: &[u8]) -> Result<Vec<Tag>> {
|
||||
parse_binary_with_limit(b, max_tags())
|
||||
}
|
||||
|
||||
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();
|
||||
while c.p < b.len() {
|
||||
let mut code = i32::from(c.i16()?);
|
||||
if code == 255 {
|
||||
code = 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 {
|
||||
let _ = write!(s, "{x:02X}");
|
||||
}
|
||||
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 {
|
||||
if !v.is_finite() {
|
||||
return "0.0".into();
|
||||
}
|
||||
let s = format!("{v}");
|
||||
if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") { s } else { format!("{s}.0") }
|
||||
}
|
||||
|
||||
/// 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 CADCraft 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 = ascii(tags, Strings::Escaped(codepage::encoder(code_page)));
|
||||
if !bytes.is_ascii() && std::str::from_utf8(&bytes).is_ok() {
|
||||
return ascii(tags, Strings::Escaped(None));
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let _ = write!(out, "{:>3}\r\n", t.code);
|
||||
match &t.value {
|
||||
Value::Str(s) => push_string(s, strings, &mut out),
|
||||
Value::Real(r) => out.extend_from_slice(format_real(*r).as_bytes()),
|
||||
Value::Int(i) => out.extend_from_slice(i.to_string().as_bytes()),
|
||||
Value::Bool(b) => out.extend_from_slice(i64::from(*b).to_string().as_bytes()),
|
||||
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, _) 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> {
|
||||
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().trim_end() == "SECTION");
|
||||
if is_sec {
|
||||
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().trim_end() == "ENDSEC")) {
|
||||
j += 1;
|
||||
}
|
||||
out.push(Section { name, tags: tags.get(start..j).unwrap_or(&[]).to_vec() });
|
||||
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>)> {
|
||||
let mut out: Vec<(String, Vec<Tag>)> = Vec::new();
|
||||
for t in tags {
|
||||
if t.code == 0 {
|
||||
out.push((t.str(), Vec::new()));
|
||||
} else if let Some(last) = out.last_mut() {
|
||||
last.1.push(t.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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_eq!(t[0].f64(), 0.0);
|
||||
}
|
||||
|
||||
#[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}");
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
//! The DXF/DWG file versions CADCraft writes (DXF Reference, HEADER `$ACADVER`).
|
||||
|
||||
/// A file version CADCraft can save: the AutoCAD release that introduced the format. Each format
|
||||
/// also serves the releases after it (2018 is still AutoCAD's current format).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Version {
|
||||
/// AutoCAD 2000/2002 (AC1015).
|
||||
R2000,
|
||||
/// AutoCAD 2004–2006 (AC1018).
|
||||
R2004,
|
||||
/// AutoCAD 2007–2009 (AC1021): the first version whose DXF text is UTF-8.
|
||||
R2007,
|
||||
/// AutoCAD 2010–2012 (AC1024).
|
||||
R2010,
|
||||
/// AutoCAD 2013–2017 (AC1027).
|
||||
R2013,
|
||||
/// AutoCAD 2018 and later (AC1032), the default.
|
||||
#[default]
|
||||
R2018,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// Every version, oldest first.
|
||||
pub const ALL: [Version; 6] = [Version::R2000, Version::R2004, Version::R2007, Version::R2010, Version::R2013, Version::R2018];
|
||||
|
||||
/// The `$ACADVER` code ("AC1032").
|
||||
pub fn code(self) -> &'static str {
|
||||
match self {
|
||||
Version::R2000 => "AC1015",
|
||||
Version::R2004 => "AC1018",
|
||||
Version::R2007 => "AC1021",
|
||||
Version::R2010 => "AC1024",
|
||||
Version::R2013 => "AC1027",
|
||||
Version::R2018 => "AC1032",
|
||||
}
|
||||
}
|
||||
|
||||
/// The release year that names the version ("2018").
|
||||
pub fn year(self) -> &'static str {
|
||||
match self {
|
||||
Version::R2000 => "2000",
|
||||
Version::R2004 => "2004",
|
||||
Version::R2007 => "2007",
|
||||
Version::R2010 => "2010",
|
||||
Version::R2013 => "2013",
|
||||
Version::R2018 => "2018",
|
||||
}
|
||||
}
|
||||
|
||||
/// A version named by its year ("2018", "R2018") or its `$ACADVER` code ("AC1032"), any case.
|
||||
pub fn parse(s: &str) -> Option<Version> {
|
||||
let s = s.trim();
|
||||
let year = s.strip_prefix(['R', 'r']).unwrap_or(s);
|
||||
Version::ALL.into_iter().find(|v| v.year() == year || v.code().eq_ignore_ascii_case(s))
|
||||
}
|
||||
|
||||
/// The version to save a file of `$ACADVER` `code` in: the same version when CADCraft writes
|
||||
/// it, else the nearest one it does (R2000 for older files, 2018 for newer ones); `None` when
|
||||
/// the code isn't a version code at all.
|
||||
pub fn for_acadver(code: &str) -> Option<Version> {
|
||||
let code = code.trim();
|
||||
if let Some(v) = Version::ALL.into_iter().find(|v| v.code().eq_ignore_ascii_case(code)) {
|
||||
return Some(v);
|
||||
}
|
||||
code.get(..2).filter(|p| p.eq_ignore_ascii_case("AC"))?;
|
||||
let n = code.get(2..).filter(|d| d.bytes().all(|b| b.is_ascii_digit())).and_then(|d| d.parse::<u32>().ok())?;
|
||||
Some(match n {
|
||||
..1018 => Version::R2000,
|
||||
1018..1021 => Version::R2004,
|
||||
1021..1024 => Version::R2007,
|
||||
1024..1027 => Version::R2010,
|
||||
1027..1032 => Version::R2013,
|
||||
_ => Version::R2018,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether text in this version's DXF is UTF-8 (2007 and later). Older versions keep text in
|
||||
/// the `$DWGCODEPAGE` code page, which CADCraft avoids by escaping non-ASCII as `\U+XXXX`.
|
||||
pub fn utf8(self) -> bool {
|
||||
self >= Version::R2007
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.year())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_years_and_codes() {
|
||||
assert_eq!(Version::parse("2018"), Some(Version::R2018));
|
||||
assert_eq!(Version::parse(" r2004 "), Some(Version::R2004));
|
||||
assert_eq!(Version::parse("ac1021"), Some(Version::R2007));
|
||||
assert_eq!(Version::parse("2005"), None);
|
||||
assert_eq!(Version::parse(""), None);
|
||||
for v in Version::ALL {
|
||||
assert_eq!(Version::parse(v.code()), Some(v));
|
||||
assert_eq!(Version::parse(v.year()), Some(v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_versions_map_to_the_nearest_writable_one() {
|
||||
assert_eq!(Version::for_acadver("AC1009"), Some(Version::R2000));
|
||||
assert_eq!(Version::for_acadver("AC1014"), Some(Version::R2000));
|
||||
assert_eq!(Version::for_acadver("AC1024"), Some(Version::R2010));
|
||||
assert_eq!(Version::for_acadver("AC1035"), Some(Version::R2018));
|
||||
assert_eq!(Version::for_acadver("MC0.0"), None);
|
||||
assert_eq!(Version::for_acadver("AC"), None);
|
||||
assert_eq!(Version::for_acadver("AC99999999999999999999"), None);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//! A UTF-8 byte order mark at the start of an ASCII DXF is skipped.
|
||||
|
||||
const BODY: &str = " 0\r\nSECTION\r\n 2\r\nENTITIES\r\n 0\r\nENDSEC\r\n 0\r\nEOF\r\n";
|
||||
|
||||
#[test]
|
||||
fn utf8_bom_bytes_are_skipped() {
|
||||
let plain = cadcraft_dxf::parse(BODY.as_bytes()).expect("plain parses");
|
||||
let mut bytes = vec![0xEF, 0xBB, 0xBF];
|
||||
bytes.extend_from_slice(BODY.as_bytes());
|
||||
let with_bom = cadcraft_dxf::parse(&bytes);
|
||||
assert!(with_bom.is_ok(), "{with_bom:?}");
|
||||
assert_eq!(with_bom.map(|t| t.len()).unwrap_or(0), plain.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bom_text_is_skipped() {
|
||||
let text = format!("\u{feff}{BODY}");
|
||||
assert!(cadcraft_dxf::parse_ascii(&text).is_ok());
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//! Binary DXF writing (#421): every value kind round-trips through `write_binary` and `parse`,
|
||||
//! and hostile binary input never panics.
|
||||
|
||||
use cadcraft_dxf::{Tag, Value, is_binary, parse, write_ascii_as, write_binary};
|
||||
|
||||
fn sample() -> Vec<Tag> {
|
||||
vec![
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "ENTITIES"),
|
||||
Tag::s(0, "TEXT"),
|
||||
Tag::s(5, "1F"),
|
||||
Tag::f(10, -3.25),
|
||||
Tag::i(62, 5),
|
||||
Tag::i(90, 70_000),
|
||||
Tag::i(160, 1 << 40),
|
||||
Tag::new(290, Value::Bool(true)),
|
||||
Tag::new(310, Value::Hex("00FFA5".into())),
|
||||
Tag::s(1, "图号 Café 😀"),
|
||||
Tag::i(1071, -2),
|
||||
Tag::s(0, "ENDSEC"),
|
||||
Tag::s(0, "EOF"),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_roundtrips_every_value_kind() {
|
||||
for utf8 in [false, true] {
|
||||
let bytes = write_binary(&sample(), utf8);
|
||||
assert!(is_binary(&bytes));
|
||||
assert_eq!(parse(&bytes).unwrap(), sample(), "utf8 = {utf8}");
|
||||
}
|
||||
// Pre-2007 strings are escaped to ASCII, 2007+ ones are UTF-8.
|
||||
let old = write_binary(&sample(), false);
|
||||
assert!(old.windows(7).any(|w| w == b"\\U+56FE"));
|
||||
let new = write_binary(&sample(), true);
|
||||
assert!(new.windows("图号".len()).any(|w| w == "图号".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_converts_values_to_their_codes_kind() {
|
||||
// A number held as text at a numeric code, an out-of-range 16-bit value, a long chunk.
|
||||
let long = "AB".repeat(300);
|
||||
let tags = vec![Tag::s(70, "12"), Tag::i(62, 1 << 20), Tag::new(310, Value::Hex(long)), Tag::s(0, "EOF")];
|
||||
let back = parse(&write_binary(&tags, true)).unwrap();
|
||||
assert_eq!(back[0], Tag::i(70, 12));
|
||||
assert_eq!(back[1], Tag::i(62, i64::from(i16::MAX)));
|
||||
assert_eq!(back[2].code, 310);
|
||||
assert_eq!(back[3].code, 310, "300 bytes are split into chunks of at most 255");
|
||||
assert_eq!(back[2].str().len() + back[3].str().len(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_ascii_keeps_text_and_drops_line_breaks() {
|
||||
let text = write_ascii_as(&[Tag::s(1, "图号\r\nÄ"), Tag::s(0, "EOF")], true);
|
||||
assert!(text.contains("图号 Ä\r\n"));
|
||||
assert_eq!(parse(text.as_bytes()).unwrap()[0].str(), "图号 Ä");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_binary_input_never_panics() {
|
||||
let good = write_binary(&sample(), true);
|
||||
// Every truncation, and every single-byte corruption of the body.
|
||||
for n in 0..good.len() {
|
||||
let _ = parse(good.get(..n).unwrap());
|
||||
}
|
||||
for i in 22..good.len() {
|
||||
for b in [0u8, 0x7f, 0xff] {
|
||||
let mut bad = good.clone();
|
||||
bad[i] = b;
|
||||
let _ = parse(&bad);
|
||||
}
|
||||
}
|
||||
// A sentinel and nothing else is not a drawing.
|
||||
assert!(parse(b"AutoCAD Binary DXF\r\n\x1a\0").is_err());
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
//! Pre-R2007 DXF text is written in the `$DWGCODEPAGE` code page: characters it has as their
|
||||
//! bytes, the others as `\U+XXXX` escapes; every form reads back to the same text.
|
||||
|
||||
use cadcraft_dxf::{Tag, parse, write_ascii, write_ascii_in, write_binary_in};
|
||||
|
||||
fn file(code_page: &str, text: &str) -> Vec<Tag> {
|
||||
vec![
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "HEADER"),
|
||||
Tag::s(9, "$ACADVER"),
|
||||
Tag::s(1, "AC1015"),
|
||||
Tag::s(9, "$DWGCODEPAGE"),
|
||||
Tag::s(3, code_page),
|
||||
Tag::s(0, "ENDSEC"),
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "ENTITIES"),
|
||||
Tag::s(0, "TEXT"),
|
||||
Tag::s(1, text),
|
||||
Tag::s(0, "ENDSEC"),
|
||||
Tag::s(0, "EOF"),
|
||||
]
|
||||
}
|
||||
|
||||
fn contains(bytes: &[u8], part: &[u8]) -> bool {
|
||||
bytes.windows(part.len()).any(|w| w == part)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_is_written_in_the_code_page() {
|
||||
// (code page, text, the bytes written for it)
|
||||
let cases: [(&str, &str, &[u8]); 6] = [
|
||||
("ANSI_1252", "Ação € 图", b"A\xE7\xE3o \x80 \\U+56FE"),
|
||||
("ANSI_1251", "Слой é", b"\xD1\xEB\xEE\xE9 \\U+00E9"),
|
||||
("ANSI_932", "表図面 é", b"\x95\x5C\x90\x7D\x96\xCA \\U+00E9"),
|
||||
("DOS850", "Größe €", b"Gr\x94\xE1e \\U+20AC"),
|
||||
("ANSI_1252", "😀", b"\\U+D83D\\U+DE00"),
|
||||
// A code page CADCraft doesn't know: everything non-ASCII is escaped.
|
||||
("ANSI_1361", "Ação", b"A\\U+00E7\\U+00E3o"),
|
||||
];
|
||||
for (cp, text, raw) in cases {
|
||||
let tags = file(cp, text);
|
||||
for (bytes, what) in [(write_ascii_in(&tags, cp), "ASCII"), (write_binary_in(&tags, cp), "binary")] {
|
||||
assert!(contains(&bytes, raw), "{cp} {what}: {:?}", String::from_utf8_lossy(&bytes));
|
||||
assert_eq!(parse(&bytes).unwrap()[10].str(), text, "{cp} {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Code page bytes that happen to form valid UTF-8 (`СІ` in ANSI_1251 is D1 B2, which is U+0472
|
||||
/// in UTF-8) would be read back as UTF-8 from an ASCII file, so that file is written with
|
||||
/// escapes. Binary DXF reads strings by the header's code page and keeps the bytes.
|
||||
#[test]
|
||||
fn code_page_bytes_that_are_valid_utf8_are_escaped_in_ascii() {
|
||||
let tags = file("ANSI_1251", "СІ");
|
||||
let ascii = write_ascii_in(&tags, "ANSI_1251");
|
||||
assert!(ascii.is_ascii(), "{:?}", String::from_utf8_lossy(&ascii));
|
||||
assert!(contains(&ascii, b"\\U+0421\\U+0406"));
|
||||
assert_eq!(parse(&ascii).unwrap()[10].str(), "СІ");
|
||||
let binary = write_binary_in(&tags, "ANSI_1251");
|
||||
assert!(contains(&binary, b"\xD1\xB2"));
|
||||
assert_eq!(parse(&binary).unwrap()[10].str(), "СІ");
|
||||
// Bytes that are not valid UTF-8 stay as they are.
|
||||
assert!(contains(&write_ascii_in(&file("ANSI_1251", "Слой"), "ANSI_1251"), b"\xD1\xEB\xEE\xE9"));
|
||||
}
|
||||
|
||||
/// A literal `\U+` or `\M+` in the text (#2050) reads back as written alongside code page bytes,
|
||||
/// in ASCII and binary, including an ASCII file that falls back to escapes because its bytes
|
||||
/// would read as UTF-8.
|
||||
#[test]
|
||||
fn literal_escapes_survive_code_page_text() {
|
||||
for (cp, text) in
|
||||
[("ANSI_1252", "Ação \\U+00E9 \\M+183A4F"), ("ANSI_1251", "Слой \\U+0041"), ("ANSI_1251", "СІ \\U+0041"), ("ANSI_932", "表 \\M+1")]
|
||||
{
|
||||
let tags = file(cp, text);
|
||||
for (bytes, what) in [(write_ascii_in(&tags, cp), "ASCII"), (write_binary_in(&tags, cp), "binary")] {
|
||||
assert!(contains(&bytes, b"\\U+005C"), "{cp} {what}: {:?}", String::from_utf8_lossy(&bytes));
|
||||
assert_eq!(parse(&bytes).unwrap()[10].str(), text, "{cp} {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-byte code pages whose trail byte can be 0x5C (`\\`): Shift-JIS `表` (95 5C) and `ソ`
|
||||
/// (83 5C), Big5 `許` (B3 5C), GBK `乗` (81 5C). Right before `U+` or `M+`, such a character is
|
||||
/// written as an escape, so even a reader that looks for escapes before it decodes the code page
|
||||
/// reads it right. Elsewhere it stays as its bytes. Both forms read back as written.
|
||||
#[test]
|
||||
fn a_trail_byte_backslash_never_starts_an_escape() {
|
||||
let cases: [(&str, &str, &[u8], &[u8]); 4] = [
|
||||
("ANSI_932", "表U+0041 表示", b"\\U+8868U+0041 \x95\x5C\x8E\xA6", b"\x95\x5CU+"),
|
||||
("ANSI_932", "ソM+1A2B", b"\\U+30BDM+1A2B", b"\x83\x5CM+"),
|
||||
("ANSI_950", "許U+0041", b"\\U+8A31U+0041", b"\xB3\x5CU+"),
|
||||
("ANSI_936", "乗U+0041", b"\\U+4E57U+0041", b"\x81\x5CU+"),
|
||||
];
|
||||
for (cp, text, written, never) in cases {
|
||||
let tags = file(cp, text);
|
||||
for (bytes, what) in [(write_ascii_in(&tags, cp), "ASCII"), (write_binary_in(&tags, cp), "binary")] {
|
||||
assert!(contains(&bytes, written), "{cp} {what}: {:?}", String::from_utf8_lossy(&bytes));
|
||||
assert!(!contains(&bytes, never), "{cp} {what}: a trail byte starts an escape");
|
||||
assert_eq!(parse(&bytes).unwrap()[10].str(), text, "{cp} {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
//! Small independent byte fixtures for DXF tag types, text encoding, framing and failures.
|
||||
//! Handwritten examples, not files from a CAD application or generated by our writer.
|
||||
//! Background on the tag model and file structure:
|
||||
//! https://ezdxf.readthedocs.io/en/stable/dxfinternals/dxftags.html
|
||||
//! https://ezdxf.readthedocs.io/en/stable/dxfinternals/filestructure.html
|
||||
//! ASCII spacing is our writer's documented convention; binary fixtures specify actual bytes.
|
||||
|
||||
use cadcraft_dxf::{DxfError, Tag, Value, parse, parse_with_limit, write_ascii_as, write_binary};
|
||||
|
||||
const SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0";
|
||||
|
||||
fn fixtures() -> Vec<(Tag, &'static [u8], &'static [u8])> {
|
||||
vec![
|
||||
(Tag::s(1, " A "), b" 1\r\n A \r\n", b"\x01\0 A \0"),
|
||||
(Tag::s(1, ""), b" 1\r\n\r\n", b"\x01\0\0"),
|
||||
(Tag::s(105, "AB12"), b"105\r\nAB12\r\n", b"\x69\0AB12\0"),
|
||||
(Tag::new(10, Value::Real(-3.25)), b" 10\r\n-3.25\r\n", b"\x0a\0\0\0\0\0\0\0\x0a\xc0"),
|
||||
(Tag::i(70, -32768), b" 70\r\n-32768\r\n", b"\x46\0\0\x80"),
|
||||
(Tag::i(90, -2147483648), b" 90\r\n-2147483648\r\n", b"\x5a\0\0\0\0\x80"),
|
||||
(Tag::i(160, 9223372036854775807), b"160\r\n9223372036854775807\r\n", b"\xa0\0\xff\xff\xff\xff\xff\xff\xff\x7f"),
|
||||
(Tag::new(290, Value::Bool(false)), b"290\r\n0\r\n", b"\x22\x01\0"),
|
||||
(Tag::new(299, Value::Bool(true)), b"299\r\n1\r\n", b"\x2b\x01\x01"),
|
||||
(Tag::new(310, Value::Hex("00FF80".into())), b"310\r\n00FF80\r\n", b"\x36\x01\x03\0\xff\x80"),
|
||||
(Tag::new(1004, Value::Hex(String::new())), b"1004\r\n\r\n", b"\xec\x03\0"),
|
||||
(Tag::i(1070, -2), b"1070\r\n-2\r\n", b"\x2e\x04\xfe\xff"),
|
||||
(Tag::i(1071, 65536), b"1071\r\n65536\r\n", b"\x2f\x04\0\0\x01\0"),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_independently_authored_ascii_and_binary_values() {
|
||||
for (expected, ascii, binary) in fixtures() {
|
||||
assert_eq!(parse(ascii).unwrap(), std::slice::from_ref(&expected), "ASCII code {}", expected.code);
|
||||
let bytes = [SENTINEL, binary].concat();
|
||||
assert_eq!(parse(&bytes).unwrap(), std::slice::from_ref(&expected), "binary code {}", expected.code);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_independently_specified_ascii_and_binary_bytes() {
|
||||
for (tag, ascii, binary) in fixtures() {
|
||||
for utf8 in [false, true] {
|
||||
let tags = std::slice::from_ref(&tag);
|
||||
assert_eq!(write_ascii_as(tags, utf8).as_bytes(), ascii, "ASCII code {}", tag.code);
|
||||
assert_eq!(write_binary(tags, utf8), [SENTINEL, binary].concat(), "binary code {}", tag.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_code_ranges_have_independent_binary_widths() {
|
||||
// Specification ranges and widths, never kind_of(): a shared reader/writer mistake
|
||||
// can round-trip every boundary value while still using the wrong number of bytes.
|
||||
let widths = [
|
||||
(vec![60..=79, 170..=179, 270..=289, 370..=389, 400..=409, 1060..=1070], 2, i64::from(i16::MIN), i64::from(i16::MAX)),
|
||||
(vec![90..=99, 420..=429, 440..=459, 1071..=1071], 4, i64::from(i32::MIN), i64::from(i32::MAX)),
|
||||
(std::iter::once(160..=169).collect(), 8, i64::MIN, i64::MAX),
|
||||
];
|
||||
for (ranges, width, min, max) in widths {
|
||||
for code in ranges.into_iter().flatten() {
|
||||
for value in [min, min + 1, -1, 0, 1, max - 1, max] {
|
||||
// Binary DXF uses a little-endian 16-bit group code and a fixed-width,
|
||||
// little-endian two's-complement integer. Encoding is version-independent.
|
||||
let code_bytes = u16::try_from(code).unwrap().to_le_bytes();
|
||||
let value_bytes = value.to_le_bytes();
|
||||
let expected = [SENTINEL, &code_bytes, &value_bytes[..width]].concat();
|
||||
let tags = [Tag::i(code, value)];
|
||||
for utf8 in [false, true] {
|
||||
assert_eq!(write_binary(&tags, utf8), expected, "code={code} value={value} utf8={utf8}");
|
||||
}
|
||||
assert_eq!(parse(&expected).unwrap(), tags, "code={code} value={value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_encoding_has_independent_wire_expectations() {
|
||||
let tags = [Tag::s(1, "é😀")];
|
||||
let escaped = b"\\U+00E9\\U+D83D\\U+DE00";
|
||||
let utf8 = b"\xc3\xa9\xf0\x9f\x98\x80";
|
||||
for (unicode, text) in [(false, escaped.as_slice()), (true, utf8.as_slice())] {
|
||||
let ascii = [b" 1\r\n".as_slice(), text, b"\r\n"].concat();
|
||||
let binary = [SENTINEL, b"\x01\0", text, b"\0"].concat();
|
||||
assert_eq!(write_ascii_as(&tags, unicode).as_bytes(), ascii, "utf8={unicode}");
|
||||
assert_eq!(write_binary(&tags, unicode), binary, "utf8={unicode}");
|
||||
assert_eq!(parse(&ascii).unwrap(), tags);
|
||||
assert_eq!(parse(&binary).unwrap(), tags);
|
||||
}
|
||||
// Pre-2007 code-page bytes are independent of the writer, which emits Unicode escapes.
|
||||
let ascii = b"0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DWGCODEPAGE\n3\nANSI_1252\n0\nENDSEC\n1\n\x80\xe9\n0\nEOF\n";
|
||||
let binary = [
|
||||
SENTINEL,
|
||||
b"\0\0SECTION\0\x02\0HEADER\0\x09\0$ACADVER\0\x01\0AC1015\0\x09\0$DWGCODEPAGE\0\x03\0ANSI_1252\0\0\0ENDSEC\0\x01\0\x80\xe9\0\0\0EOF\0",
|
||||
]
|
||||
.concat();
|
||||
for bytes in [ascii.as_slice(), binary.as_slice()] {
|
||||
let tags = parse(bytes).unwrap();
|
||||
assert_eq!(tags[7], Tag::s(1, "ێ"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_accepts_line_endings_padding_and_no_final_newline() {
|
||||
let expected = [Tag::i(70, -7), Tag::s(1, " text "), Tag::s(0, "EOF")];
|
||||
for newline in ["\n", "\r\n"] {
|
||||
for final_newline in [false, true] {
|
||||
let mut text = [" 70 ", " -7 ", "1", " text ", "0", "EOF"].join(newline);
|
||||
if final_newline {
|
||||
text.push_str(newline);
|
||||
}
|
||||
assert_eq!(parse(text.as_bytes()).unwrap(), expected, "newline={newline:?} final={final_newline}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_and_truncated_values_are_errors_not_panics() {
|
||||
assert!(matches!(parse(b""), Err(DxfError::NotDxf)));
|
||||
assert!(matches!(parse(b"oops\nvalue\n"), Err(DxfError::BadCode { line: 1, .. })));
|
||||
assert!(matches!(parse(b"10\n"), Err(DxfError::Eof)));
|
||||
assert!(matches!(parse(SENTINEL), Err(DxfError::NotDxf)));
|
||||
for (_, _, binary) in fixtures() {
|
||||
// Every cut inside the first record is an error, unlike cuts between complete records:
|
||||
// the tag API deliberately accepts streams without an EOF record.
|
||||
for cut in 1..binary.len() {
|
||||
let bytes = [SENTINEL, &binary[..cut]].concat();
|
||||
assert!(matches!(parse(&bytes), Err(DxfError::Eof)), "binary={binary:?} cut={cut}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_limit_accepts_the_exact_boundary_and_rejects_one_less() {
|
||||
let ascii = b"1\nA\n70\n-7\n0\nEOF\n";
|
||||
let binary = [SENTINEL, b"\x01\0A\0\x46\0\xf9\xff\0\0EOF\0"].concat();
|
||||
for bytes in [ascii.as_slice(), binary.as_slice()] {
|
||||
for limit in 0..3 {
|
||||
assert!(matches!(parse_with_limit(bytes, limit), Err(DxfError::TooLarge { limit: actual, .. }) if actual == limit));
|
||||
}
|
||||
for limit in [3, 4] {
|
||||
assert_eq!(parse_with_limit(bytes, limit).unwrap(), [Tag::s(1, "A"), Tag::i(70, -7), Tag::s(0, "EOF")]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
//! 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_uses_utf8_per_line_and_binary_stays_in_the_code_page() {
|
||||
// ANSI_1252 file with UTF-8 bytes in one string and a Latin-1 byte in another. The ASCII
|
||||
// reader takes a line that is valid UTF-8 as UTF-8; the binary reader reads every string
|
||||
// from the code page, because a pre-R2007 binary file has no way to escape to UTF-8.
|
||||
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));
|
||||
// Clamped to what the group's width holds, unsigned values included (the binary writer
|
||||
// keeps up to 65535 and 4294967295 as their bit patterns).
|
||||
assert_eq!(t[2], Tag::i(70, i64::from(u16::MAX)));
|
||||
assert_eq!(t[3], Tag::i(70, i64::from(i16::MIN)));
|
||||
assert_eq!(t[4], Tag::i(90, i64::from(u32::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);
|
||||
}
|
||||
|
||||
/// An unsigned value spelled as a float reads as the same spelled as an integer (#1026 review):
|
||||
/// `65535.0` is 65535 like `65535`, not 32767.
|
||||
#[test]
|
||||
fn unsigned_values_read_the_same_in_either_spelling() {
|
||||
let t = parse(b"70\n65535.0\n70\n65535\n90\n4294967295.0\n90\n4294967295\n0\nEOF\n").unwrap();
|
||||
assert_eq!(t[0], t[1]);
|
||||
assert_eq!(t[0], Tag::i(70, 65535));
|
||||
assert_eq!(t[2], t[3]);
|
||||
assert_eq!(t[2], Tag::i(90, 4_294_967_295));
|
||||
}
|
||||
|
||||
#[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)));
|
||||
}
|
||||
|
||||
/// Strings of a `$DWGCODEPAGE`-declared file as ASCII text and as a binary file.
|
||||
fn both_forms(code_page: &str, strings: &[&[u8]]) -> [Vec<Tag>; 2] {
|
||||
let mut a = format!("9\n$DWGCODEPAGE\n3\n{code_page}\n").into_bytes();
|
||||
for s in strings {
|
||||
a.extend_from_slice(b"1\n");
|
||||
a.extend_from_slice(s);
|
||||
a.push(b'\n');
|
||||
}
|
||||
a.extend_from_slice(b"0\nEOF\n");
|
||||
let b = bin_with(|b| {
|
||||
for (code, s) in [(9i16, &b"$DWGCODEPAGE"[..]), (3, code_page.as_bytes())].into_iter().chain(strings.iter().map(|s| (1, *s))) {
|
||||
b.extend_from_slice(&code.to_le_bytes());
|
||||
b.extend_from_slice(s);
|
||||
b.push(0);
|
||||
}
|
||||
});
|
||||
[parse(&a).unwrap(), parse(&b).unwrap()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn east_asian_short_strings_that_look_like_utf8_stay_in_the_code_page() {
|
||||
// C3 B1 is valid UTF-8 ("ñ") but is also two half-width katakana in Shift-JIS; the second
|
||||
// string is plainly Shift-JIS, so the file is not UTF-8.
|
||||
for t in both_forms("ANSI_932", &[b"\xC3\xB1", b"\x90\x7D\x96\xCA"]) {
|
||||
assert_eq!(t[2].str(), "\u{FF83}\u{FF71}");
|
||||
assert_eq!(t[3].str(), "図面");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn east_asian_file_that_is_all_utf8_reads_as_utf8() {
|
||||
for t in both_forms("ANSI_932", &["図面".as_bytes(), b"\xC3\xB1", b"plain"]) {
|
||||
assert_eq!(t[2].str(), "図面");
|
||||
assert_eq!(t[3].str(), "ñ");
|
||||
assert_eq!(t[4].str(), "plain");
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
//! Hex payload conformance is distinct from binary framing capacity: DXF groups 310–319 and
|
||||
//! 1004 specify at most 127 payload bytes per chunk, even though a length byte represents 255.
|
||||
//! Inspect writer bytes without our parser; accept any compliant partition preserving payload,
|
||||
//! code and following-record framing. Oversized binary read fixtures test tolerance only.
|
||||
//! Pending writer regressions extend the chunk-size finding in issue #1025.
|
||||
//! Public DXF Reference, Group Codes in Numerical Order and Binary DXF Files:
|
||||
//! https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-DXF/files/GUID-3F0380A5-1C15-464D-BC66-2C5F094BCFB9.htm
|
||||
//! https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-DXF/files/GUID-FC1C3C69-DBC2-49E4-893A-000D6538C0FE.htm
|
||||
|
||||
use cadcraft_dxf::{Tag, Value, parse, write_ascii_as, write_binary};
|
||||
|
||||
const SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0";
|
||||
const FOLLOWING_BINARY: &[u8] = b"\x46\0\xf9\xff"; // group 70, signed i16 -7
|
||||
|
||||
fn payload(length: usize) -> Vec<u8> {
|
||||
(0..length).map(|index| u8::try_from(index * 37 % 256).unwrap()).collect()
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02X}")).collect()
|
||||
}
|
||||
|
||||
fn input(code: i32, bytes: &[u8]) -> [Tag; 2] {
|
||||
[Tag::new(code, Value::Hex(hex(bytes))), Tag::i(70, -7)]
|
||||
}
|
||||
|
||||
fn check_ascii(code: i32, bytes: &[u8], utf8: bool) {
|
||||
let wire = write_ascii_as(&input(code, bytes), utf8);
|
||||
let lines: Vec<_> = wire.lines().collect();
|
||||
let (chunks, following) = lines.split_at(lines.len().checked_sub(2).unwrap());
|
||||
assert_eq!(following[0].trim(), "70");
|
||||
assert_eq!(following[1], "-7");
|
||||
assert!(!chunks.is_empty(), "an empty payload must still have a tag");
|
||||
let (records, remainder) = chunks.as_chunks::<2>();
|
||||
assert!(remainder.is_empty(), "incomplete group/value pair");
|
||||
let context = format!("ASCII code={code} bytes={} utf8={utf8}", bytes.len());
|
||||
let mut back = Vec::new();
|
||||
for record in records {
|
||||
assert_eq!(record[0].trim().parse::<i32>().unwrap(), code, "{context}");
|
||||
let text = record[1];
|
||||
assert!(text.len() <= 254, "{context}: {} hex characters exceed 127 payload bytes", text.len());
|
||||
assert!(text.bytes().all(|b| b.is_ascii_hexdigit()), "{context}: invalid hex");
|
||||
let (pairs, remainder) = text.as_bytes().as_chunks::<2>();
|
||||
assert!(remainder.is_empty(), "{context}: partial byte");
|
||||
back.extend(pairs.iter().map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()));
|
||||
}
|
||||
assert_eq!(back, bytes, "{context}: complete payload and byte order");
|
||||
}
|
||||
|
||||
fn check_binary(code: i32, bytes: &[u8], utf8: bool) {
|
||||
let wire = write_binary(&input(code, bytes), utf8);
|
||||
let mut chunks = wire.strip_prefix(SENTINEL).unwrap().strip_suffix(FOLLOWING_BINARY).unwrap();
|
||||
assert!(!chunks.is_empty(), "an empty payload must still have a tag");
|
||||
let context = format!("binary code={code} bytes={} utf8={utf8}", bytes.len());
|
||||
let mut back = Vec::new();
|
||||
while !chunks.is_empty() {
|
||||
let (&[lo, hi, length], rest) = chunks.split_first_chunk::<3>().unwrap();
|
||||
assert_eq!(i32::from(u16::from_le_bytes([lo, hi])), code, "{context}");
|
||||
assert!(length <= 127, "{context}: {length} bytes exceed the documented group limit");
|
||||
let (chunk, next) = rest.split_at(usize::from(length));
|
||||
back.extend_from_slice(chunk);
|
||||
chunks = next;
|
||||
}
|
||||
assert_eq!(back, bytes, "{context}: complete payload and byte order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conforming_payloads_have_independent_ascii_and_binary_wire_checks() {
|
||||
for code in (310..=319).chain([1004]) {
|
||||
for length in [0, 1, 2, 126, 127] {
|
||||
for utf8 in [false, true] {
|
||||
let bytes = payload(length);
|
||||
check_ascii(code, &bytes, utf8);
|
||||
check_binary(code, &bytes, utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_reader_tolerates_the_full_length_byte_domain() {
|
||||
// This is deliberately permissive input, not an assertion that oversized chunks conform
|
||||
// or that the writer should produce them. The following record catches framing mistakes.
|
||||
for code in (310_u16..=319).chain([1004]) {
|
||||
for length in 0..=u8::MAX {
|
||||
let bytes = payload(usize::from(length));
|
||||
let mut wire = SENTINEL.to_vec();
|
||||
wire.extend_from_slice(&code.to_le_bytes());
|
||||
wire.push(length);
|
||||
wire.extend_from_slice(&bytes);
|
||||
wire.extend_from_slice(FOLLOWING_BINARY);
|
||||
assert_eq!(parse(&wire).unwrap(), input(i32::from(code), &bytes), "code={code} length={length}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Confirmed ASCII DXF writer nonconformance: hex payloads above 127 bytes are emitted as one oversized chunk"]
|
||||
fn ascii_writer_splits_large_payloads_into_conforming_chunks() {
|
||||
for code in (310..=319).chain([1004]) {
|
||||
for length in [128, 254, 255, 256, 1024] {
|
||||
for utf8 in [false, true] {
|
||||
check_ascii(code, &payload(length), utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Confirmed binary DXF writer nonconformance: 255-byte partitioning exceeds the documented 127-byte group limit"]
|
||||
fn binary_writer_splits_large_payloads_into_conforming_chunks() {
|
||||
for code in (310..=319).chain([1004]) {
|
||||
for length in [128, 254, 255, 256, 1024] {
|
||||
for utf8 in [false, true] {
|
||||
check_binary(code, &payload(length), utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//! Deterministic tag preservation through the public byte API, in both encodings and every
|
||||
//! supported version. Inputs have the group code's type and representable values; lossy
|
||||
//! coercions are not accepted as round-trip equality. Real values must retain their bits.
|
||||
|
||||
use cadcraft_dxf::{Tag, Value, Version, is_binary, parse, write_ascii_as, write_binary};
|
||||
|
||||
fn check(case: &str, body: &[Tag]) {
|
||||
for version in Version::ALL {
|
||||
let mut original = vec![
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "HEADER"),
|
||||
Tag::s(9, "$ACADVER"),
|
||||
Tag::s(1, version.code()),
|
||||
Tag::s(9, "$DWGCODEPAGE"),
|
||||
Tag::s(3, "ANSI_1252"),
|
||||
Tag::s(0, "ENDSEC"),
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "ENTITIES"),
|
||||
];
|
||||
original.extend_from_slice(body);
|
||||
original.extend([Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]);
|
||||
for binary in [false, true] {
|
||||
let mut current = original.clone();
|
||||
for pass in 1..=2 {
|
||||
let context = format!("{case} · {version} · binary={binary} · pass={pass}");
|
||||
let bytes = if binary { write_binary(¤t, version.utf8()) } else { write_ascii_as(¤t, version.utf8()).into_bytes() };
|
||||
assert_eq!(is_binary(&bytes), binary, "{context}");
|
||||
let back = parse(&bytes).unwrap_or_else(|e| panic!("{context}: {e}"));
|
||||
assert_eq!(back.len(), original.len(), "{context}: tag count");
|
||||
for (index, (expected, actual)) in original.iter().zip(&back).enumerate() {
|
||||
assert_eq!(actual, expected, "{context}: tag[{index}]");
|
||||
if let (Value::Real(a), Value::Real(b)) = (&expected.value, &actual.value) {
|
||||
assert_eq!(b.to_bits(), a.to_bits(), "{context}: tag[{index}] real bits");
|
||||
}
|
||||
}
|
||||
assert_eq!(back, current, "{context}: no additional changes on resave");
|
||||
current = back;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_width_boundaries() {
|
||||
// Explicit reference ranges, not obtained from the codec's kind_of().
|
||||
let widths = [
|
||||
(vec![60..=79, 170..=179, 270..=289, 370..=389, 400..=409, 1060..=1070], i64::from(i16::MIN), i64::from(i16::MAX)),
|
||||
(vec![90..=99, 420..=429, 440..=459, 1071..=1071], i64::from(i32::MIN), i64::from(i32::MAX)),
|
||||
(std::iter::once(160..=169).collect(), i64::MIN, i64::MAX),
|
||||
];
|
||||
for (ranges, min, max) in widths {
|
||||
for value in [min, min + 1, -1, 0, 1, max - 1, max] {
|
||||
let tags: Vec<_> = ranges.iter().cloned().flatten().map(|code| Tag::i(code, value)).collect();
|
||||
check(&format!("integer/{min}..{max}/{value}"), &tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_reals_keep_all_bits() {
|
||||
let values = [
|
||||
0.0,
|
||||
-0.0,
|
||||
f64::from_bits(1),
|
||||
-f64::from_bits(1),
|
||||
f64::MIN_POSITIVE,
|
||||
f64::MIN_POSITIVE.next_down(),
|
||||
0.1,
|
||||
-3.25,
|
||||
1.0_f64.next_down(),
|
||||
1.0,
|
||||
1.0_f64.next_up(),
|
||||
1e-100,
|
||||
-1e100,
|
||||
f64::MAX,
|
||||
f64::MIN,
|
||||
];
|
||||
for value in values {
|
||||
let tags: Vec<_> =
|
||||
[10..=59, 110..=149, 210..=239, 460..=469, 1010..=1059].into_iter().flatten().map(|code| Tag::new(code, Value::Real(value))).collect();
|
||||
check(&format!("real/{:016X}", value.to_bits()), &tags);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn booleans() {
|
||||
for value in [false, true] {
|
||||
let tags: Vec<_> = (290..=299).map(|code| Tag::new(code, Value::Bool(value))).collect();
|
||||
check(&format!("bool/{value}"), &tags);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_preserve_whitespace_unicode_and_literal_escapes() {
|
||||
// No CR/LF or NUL: those are line/string delimiters, not losslessly writable payloads.
|
||||
let values = [
|
||||
("empty", ""),
|
||||
("spaces", " leading and trailing "),
|
||||
("tabs", "\tindented\t"),
|
||||
("unicode", "Café Ø 图号 Україна 😀 e\u{301}"),
|
||||
("literal-escapes", r"\U+0041 \M+183A4 \U+D83D\U+DE00 \\U+005C \P %%c"),
|
||||
("malformed-escapes", r"\U+ \U+ZZZZ \U+D800 \U+DC00 \M+9FFFF"),
|
||||
];
|
||||
for (case, value) in values {
|
||||
let tags: Vec<_> =
|
||||
[0..=9, 100..=100, 102..=102, 105..=105, 300..=309, 320..=369, 410..=419, 430..=439, 470..=479, 480..=481, 1000..=1003, 1005..=1005]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|code| Tag::s(code, value))
|
||||
.collect();
|
||||
check(case, &tags);
|
||||
}
|
||||
for length in [1, 254, 255, 256, 1024] {
|
||||
check(&format!("string/length={length}"), &[Tag::s(1, "x".repeat(length))]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_chunks_keep_bytes_and_order() {
|
||||
// DXF groups 310–319 and 1004 permit 127 payload bytes per chunk. A binary length
|
||||
// byte can represent more, but that is reader tolerance, not writer conformance.
|
||||
// Larger payloads need chunk-size/reassembly checks rather than exact tag equality;
|
||||
// see hex_chunks.rs for the independent wire checks and pending writer regressions.
|
||||
for length in [0, 1, 2, 126, 127] {
|
||||
let hex: String = (0..length).map(|i| format!("{:02X}", (i * 37) % 256)).collect();
|
||||
let tags: Vec<_> = (310..=319).chain([1004]).map(|code| Tag::new(code, Value::Hex(hex.clone()))).collect();
|
||||
check(&format!("hex/bytes={length}"), &tags);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_values_and_repeated_codes_keep_order() {
|
||||
check(
|
||||
"mixed/repeated-codes",
|
||||
&[
|
||||
Tag::s(0, "TEST"),
|
||||
Tag::f(10, 1.25),
|
||||
Tag::s(1, "first"),
|
||||
Tag::f(10, -2.5),
|
||||
Tag::i(70, -7),
|
||||
Tag::s(1, "second"),
|
||||
Tag::new(290, Value::Bool(true)),
|
||||
Tag::new(310, Value::Hex("00FF80".into())),
|
||||
Tag::i(160, i64::MAX),
|
||||
Tag::s(1, ""),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
cadcraft-geom = { workspace = true }
|
||||
cadcraft-color = { workspace = true }
|
||||
cadcraft-doc = { workspace = true }
|
||||
cadcraft-dxf = { workspace = true }
|
||||
craft-dxf = { workspace = true }
|
||||
cadcraft-constraints = { workspace = true }
|
||||
cadcraft-fonts = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
|
||||
@@ -157,7 +157,7 @@ fn write_audit_log(_: &Session, _: &crate::audit::Report) {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn read_path(cmd: &str, path: &str) -> Result<Vec<u8>> {
|
||||
crate::capped_read::read_capped(std::path::Path::new(path), file::MAX_DRAWING_BYTES).map_err(|e| bad(cmd, format!("{path}: {e}")))
|
||||
std::fs::read(path).map_err(|e| bad(cmd, format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -85,8 +85,7 @@ fn read_drawing(path: &str) -> Result<Drawing> {
|
||||
fn read_file(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), crate::cmd::file::MAX_DRAWING_BYTES)
|
||||
.map_err(|e| bad("insert", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("insert", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("insert", "paths are not available on the web"))
|
||||
|
||||
@@ -204,8 +204,7 @@ fn import_drawing(s: &mut Session, src: &Drawing, at: Vec2, scale: f64, file: &s
|
||||
fn read_file(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), crate::cmd::file::MAX_DRAWING_BYTES)
|
||||
.map_err(|e| bad("import", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("import", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("import", "paths are not available on the web; pass `data`"))
|
||||
|
||||
@@ -8,7 +8,7 @@ mod template;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use cadcraft_doc::{Drawing, Format};
|
||||
pub use cadcraft_dxf::Version;
|
||||
pub use craft_dxf::Version;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, EngineError, Interactive, always, bad, bool_or, find_command, ok, str_param};
|
||||
@@ -24,7 +24,7 @@ pub use template::{qnew_template, set_qnew_template};
|
||||
/// endless device must be rejected before it exhausts memory (CWE-400), so native opens read
|
||||
/// through [`crate::capped_read`] rather than buffering to EOF.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) const MAX_DRAWING_BYTES: usize = 512 << 20;
|
||||
const MAX_DRAWING_BYTES: usize = 512 << 20;
|
||||
/// Largest `.scr` script accepted from a local path.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_SCRIPT_BYTES: usize = 64 << 20;
|
||||
@@ -53,9 +53,9 @@ pub fn io() -> Option<&'static IoHooks> {
|
||||
/// (installed by the app with the DXF reader and writer, a lower layer).
|
||||
pub struct EntityCodec {
|
||||
/// The entity's type name and the groups of its record, without its `0` group.
|
||||
pub groups: fn(&Drawing, &cadcraft_doc::Entity) -> (String, Vec<cadcraft_dxf::Tag>),
|
||||
pub groups: fn(&Drawing, &cadcraft_doc::Entity) -> (String, Vec<craft_dxf::Tag>),
|
||||
/// An entity from a type name and groups.
|
||||
pub entity: fn(&str, &[cadcraft_dxf::Tag]) -> Option<(cadcraft_doc::Common, cadcraft_doc::EntityKind)>,
|
||||
pub entity: fn(&str, &[craft_dxf::Tag]) -> Option<(cadcraft_doc::Common, cadcraft_doc::EntityKind)>,
|
||||
}
|
||||
|
||||
static CODEC: OnceLock<EntityCodec> = OnceLock::new();
|
||||
@@ -340,7 +340,7 @@ fn read_bytes(bytes: &[u8], name: &str, path: Option<String>) -> Result<ReadFile
|
||||
let dir = if dir.as_os_str().is_empty() { std::path::Path::new(".") } else { dir };
|
||||
cadcraft_fonts::shx::add_drawing_dir(&std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf()));
|
||||
}
|
||||
Ok(ReadFile { drawing, binary_dxf: cadcraft_dxf::is_binary(bytes), name: name.to_string(), path })
|
||||
Ok(ReadFile { drawing, binary_dxf: craft_dxf::is_binary(bytes), name: name.to_string(), path })
|
||||
}
|
||||
|
||||
fn add_read(s: &mut Session, drawing: Drawing, binary_dxf: bool, name: &str, path: Option<String>) -> usize {
|
||||
@@ -503,7 +503,7 @@ pub fn backup_path(path: &std::path::Path) -> std::path::PathBuf {
|
||||
pub fn looks_like_drawing(bytes: &[u8]) -> bool {
|
||||
// DWG: a version tag like `AC1032` (R12 is `AC1009`).
|
||||
let dwg = bytes.len() >= 6 && bytes.starts_with(b"AC") && bytes[2..6].iter().all(|b| b.is_ascii_digit());
|
||||
if dwg || cadcraft_dxf::is_binary(bytes) {
|
||||
if dwg || craft_dxf::is_binary(bytes) {
|
||||
return true;
|
||||
}
|
||||
// ASCII DXF: optional comments (999), then group 0 SECTION.
|
||||
@@ -533,7 +533,7 @@ pub fn completeness(head: &[u8], tail: &[u8], len: u64) -> Option<bool> {
|
||||
// Too short for any DWG's header: cut off, not unknown.
|
||||
return if head.len() < 256 { Some(false) } else { dwg_complete(head, len) };
|
||||
}
|
||||
if cadcraft_dxf::is_binary(head) {
|
||||
if craft_dxf::is_binary(head) {
|
||||
return Some(tail.ends_with(b"EOF\0"));
|
||||
}
|
||||
let text = String::from_utf8_lossy(tail);
|
||||
|
||||
@@ -110,7 +110,7 @@ fn other_drawing(s: &Session, p: &Value) -> Result<(Arc<Drawing>, String)> {
|
||||
fn read_path(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), super::MAX_DRAWING_BYTES).map_err(|e| bad("compare", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("compare", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("compare", "paths are not available on the web: open the other drawing and compare with it by name (`document`)"))
|
||||
|
||||
@@ -74,7 +74,7 @@ fn read_template(path: &str) -> Result<Drawing> {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn read_bytes(path: &str) -> Result<Vec<u8>> {
|
||||
crate::capped_read::read_capped(std::path::Path::new(path), super::MAX_DRAWING_BYTES).map_err(|e| bad("new", format!("template {path}: {e}")))
|
||||
std::fs::read(path).map_err(|e| bad("new", format!("template {path}: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -303,9 +303,11 @@ fn run_stylesmanager(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, Some(_path)) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let bytes = crate::capped_read::read_capped(std::path::Path::new(_path), cadcraft_doc::MAX_TABLE_FILE)
|
||||
.map_err(|e| bad(cmd, format!("{_path}: {e}")))?;
|
||||
String::from_utf8(bytes).map_err(|e| bad(cmd, format!("{_path}: {e}")))?
|
||||
let meta = std::fs::metadata(_path).map_err(|e| bad(cmd, format!("{_path}: {e}")))?;
|
||||
if meta.len() > cadcraft_doc::MAX_TABLE_FILE as u64 {
|
||||
return Err(bad(cmd, format!("{_path}: the plot style table file is too large")));
|
||||
}
|
||||
std::fs::read_to_string(_path).map_err(|e| bad(cmd, format!("{_path}: {e}")))?
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Err(bad(cmd, "files can't be read here: pass `text`"));
|
||||
|
||||
@@ -175,14 +175,8 @@ pub fn compare(prop: &Value, op: Op, value: &Value) -> bool {
|
||||
if ptxt.eq_ignore_ascii_case(vtxt.trim()) {
|
||||
return true;
|
||||
}
|
||||
// Colours: `red` = `1` = `Red`; `bylayer` = `ByLayer`. No colour spelling is long, so a
|
||||
// long text (a layer name from a file) isn't parsed, copied and split per entity.
|
||||
const MAX_COLOR_TEXT: usize = 64;
|
||||
let (pc, vc) = (ptxt.trim(), vtxt.trim());
|
||||
if pc.len() > MAX_COLOR_TEXT || vc.len() > MAX_COLOR_TEXT {
|
||||
return false;
|
||||
}
|
||||
match (Color::parse(pc), Color::parse(vc)) {
|
||||
// Colours: `red` = `1` = `Red`; `bylayer` = `ByLayer`.
|
||||
match (Color::parse(&ptxt), Color::parse(vtxt.trim())) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
@@ -406,26 +400,3 @@ mod tests {
|
||||
assert!(!wildcard(&"*a".repeat(500), &"b".repeat(4000)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod long_text_tests {
|
||||
use super::*;
|
||||
|
||||
/// A long string property is compared as text only, and a colour name still equals its index.
|
||||
#[test]
|
||||
fn long_values_skip_colour_parsing() {
|
||||
let long = Value::String("x".repeat(100_000));
|
||||
assert!(compare(&long, Op::Eq, &long));
|
||||
assert!(!compare(&long, Op::Eq, &Value::String("red".into())));
|
||||
assert!(compare(&Value::String("red".into()), Op::Eq, &Value::String("1".into())));
|
||||
}
|
||||
|
||||
/// Padding is trimmed before the length limit, so a padded colour still matches.
|
||||
#[test]
|
||||
fn padded_colours_still_match() {
|
||||
let padded = |c: &str| Value::String(format!("{}{c}{}", " ".repeat(100), " ".repeat(100)));
|
||||
assert!(compare(&padded("red"), Op::Eq, &Value::String("1".into())));
|
||||
assert!(compare(&Value::String("red".into()), Op::Eq, &padded("1")));
|
||||
assert!(!compare(&padded("red"), Op::Ne, &padded("1")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Largest linked file read (bytes).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_LINK_BYTES: usize = 8 << 20;
|
||||
const MAX_LINK_BYTES: u64 = 8 << 20;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -238,9 +238,11 @@ fn read_link(id: &str, file: &std::path::Path) -> Result<String> {
|
||||
let path = file.display();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// The limit is enforced on the bytes read, not on reported metadata, so a device or FIFO
|
||||
// that misreports its length can't be buffered to EOF.
|
||||
let bytes = crate::capped_read::read_capped(file, MAX_LINK_BYTES).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
let meta = std::fs::metadata(file).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
if meta.len() > MAX_LINK_BYTES {
|
||||
return Err(bad(id, format!("{path}: larger than {} MB", MAX_LINK_BYTES >> 20)));
|
||||
}
|
||||
let bytes = std::fs::read(file).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
Ok(text.strip_prefix('\u{feff}').unwrap_or(&text).to_owned())
|
||||
}
|
||||
@@ -368,16 +370,6 @@ fn run_update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A linked path naming an endless device is refused instead of buffered to EOF.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
#[test]
|
||||
fn link_to_endless_device_is_rejected() {
|
||||
let dev = std::path::Path::new("/dev/zero");
|
||||
if dev.exists() {
|
||||
assert!(read_link("datalink", dev).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_parsing() {
|
||||
assert_eq!(parse_csv("a,b\r\n\"c,d\",\"e\"\"f\"\n", ','), vec![vec!["a", "b"], vec!["c,d", "e\"f"]]);
|
||||
|
||||
@@ -415,12 +415,9 @@ impl Session {
|
||||
|
||||
/// Move printed text to the command history: complete lines, and with `all` the rest too.
|
||||
fn lisp_flush(&mut self, env: &mut Env, all: bool) {
|
||||
// One pass over the buffer, dropping the printed lines once at the end: draining each line
|
||||
// from the front shifted the rest every time, quadratic in newline-dense output.
|
||||
let mut done = 0;
|
||||
while let Some(i) = env.out.get(done..).and_then(|rest| rest.find('\n')) {
|
||||
let line = env.out.get(done..done + i).unwrap_or_default().trim_end_matches('\r').to_string();
|
||||
done += i + 1;
|
||||
while let Some(i) = env.out.find('\n') {
|
||||
let line: String = env.out.drain(..=i).collect();
|
||||
let line = line.trim_end_matches(['\n', '\r']).to_string();
|
||||
// `(princ "\nText")` after a prompt or a command's echo ends that line instead of
|
||||
// leaving an empty one; after LISP's own line it does print an empty line.
|
||||
if line.is_empty() && env.printed_at != Some(self.log.len()) {
|
||||
@@ -430,7 +427,6 @@ impl Session {
|
||||
self.echo(line);
|
||||
env.printed_at = Some(self.log.len());
|
||||
}
|
||||
env.out.drain(..done);
|
||||
if all && !env.out.is_empty() {
|
||||
let line = std::mem::take(&mut env.out);
|
||||
self.echo(line);
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use cadcraft_doc::{Entity, Handle, Space};
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::Vec2;
|
||||
use craft_dxf::Tag;
|
||||
|
||||
use super::LispError;
|
||||
use super::builtins::args_err;
|
||||
@@ -248,20 +248,20 @@ fn convert_angles(kind: &str, tags: &mut [Tag], f: fn(f64) -> f64) {
|
||||
continue;
|
||||
}
|
||||
let v = match t.value {
|
||||
cadcraft_dxf::Value::Real(r) => r,
|
||||
cadcraft_dxf::Value::Int(i) => i as f64,
|
||||
craft_dxf::Value::Real(r) => r,
|
||||
craft_dxf::Value::Int(i) => i as f64,
|
||||
_ => continue,
|
||||
};
|
||||
t.value = cadcraft_dxf::Value::Real(f(v));
|
||||
t.value = craft_dxf::Value::Real(f(v));
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_value(t: &Tag) -> Value {
|
||||
match &t.value {
|
||||
cadcraft_dxf::Value::Str(s) | cadcraft_dxf::Value::Hex(s) => Value::str(s),
|
||||
cadcraft_dxf::Value::Real(r) => Value::Real(*r),
|
||||
cadcraft_dxf::Value::Int(i) => Value::Int(i32::try_from(*i).unwrap_or(if *i < 0 { i32::MIN } else { i32::MAX })),
|
||||
cadcraft_dxf::Value::Bool(b) => Value::Int(i32::from(*b)),
|
||||
craft_dxf::Value::Str(s) | craft_dxf::Value::Hex(s) => Value::str(s),
|
||||
craft_dxf::Value::Real(r) => Value::Real(*r),
|
||||
craft_dxf::Value::Int(i) => Value::Int(i32::try_from(*i).unwrap_or(if *i < 0 { i32::MIN } else { i32::MAX })),
|
||||
craft_dxf::Value::Bool(b) => Value::Int(i32::from(*b)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -307,16 +307,6 @@ fn printing() {
|
||||
assert_eq!(ok(&mut s, "(rtos 12.5 2 2)"), "\"12.50\"");
|
||||
}
|
||||
|
||||
/// Output with many lines moves to the history in one pass, not one buffer shift per line.
|
||||
#[test]
|
||||
fn printing_many_lines_is_linear() {
|
||||
let mut s = session();
|
||||
let started = std::time::Instant::now();
|
||||
ok(&mut s, "(setq x \"a\\r\\n\") (repeat 17 (setq x (strcat x x))) (princ x) (princ)");
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(10), "took {:?}", started.elapsed());
|
||||
assert_eq!(s.log.last().map(String::as_str), Some("a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_draws_and_one_routine_is_one_undo_step() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -23,10 +23,10 @@ fn groups(d: &cadcraft_engine::doc::Drawing, e: &Entity) -> (String, Vec<(i32, V
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let v = match t.value {
|
||||
cadcraft_dxf::Value::Str(s) | cadcraft_dxf::Value::Hex(s) => json!(s),
|
||||
cadcraft_dxf::Value::Real(r) => json!(r),
|
||||
cadcraft_dxf::Value::Int(i) => json!(i),
|
||||
cadcraft_dxf::Value::Bool(b) => json!(i32::from(b)),
|
||||
craft_dxf::Value::Str(s) | craft_dxf::Value::Hex(s) => json!(s),
|
||||
craft_dxf::Value::Real(r) => json!(r),
|
||||
craft_dxf::Value::Int(i) => json!(i),
|
||||
craft_dxf::Value::Bool(b) => json!(i32::from(b)),
|
||||
};
|
||||
(t.code, v)
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
cadcraft-geom = { workspace = true }
|
||||
cadcraft-dxf = { workspace = true }
|
||||
craft-dxf = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
harfrust = { workspace = true }
|
||||
unicode-bidi = { workspace = true }
|
||||
|
||||
@@ -259,7 +259,7 @@ pub(crate) fn unescape_codes(s: &str) -> std::borrow::Cow<'_, str> {
|
||||
continue;
|
||||
}
|
||||
} else if n == Some('M') && plus == Some('+') && chars.get(i + 3).is_some_and(char::is_ascii_digit) && hex4(&chars, i + 4).is_some() {
|
||||
push_literal(&mut out, cadcraft_dxf::mif_escape_at(&chars, i).unwrap_or('\u{fffd}'));
|
||||
push_literal(&mut out, craft_dxf::mif_escape_at(&chars, i).unwrap_or('\u{fffd}'));
|
||||
i += 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -484,9 +484,6 @@ pub struct Polyline {
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// Sampled points one [`Polyline::tessellate`] call produces before arcs become chords.
|
||||
pub const MAX_TESSELLATED_POINTS: usize = 4_000_000;
|
||||
|
||||
impl Polyline {
|
||||
pub fn from_points(pts: &[Vec2], closed: bool) -> Self {
|
||||
Polyline { vertices: pts.iter().map(|p| PolyVertex::new(*p)).collect(), closed }
|
||||
@@ -519,24 +516,10 @@ impl Polyline {
|
||||
b
|
||||
}
|
||||
pub fn tessellate(&self, tol: f64) -> Vec<Vec2> {
|
||||
let mut budget = MAX_TESSELLATED_POINTS;
|
||||
self.tessellate_within(tol, &mut budget)
|
||||
}
|
||||
/// [`Polyline::tessellate`] spending about `budget` sampled points (shared by the caller
|
||||
/// across many polylines). The budget is checked before each segment, so the last segment
|
||||
/// started may overshoot it by that one segment's samples. Once it is spent, the remaining arcs are drawn as chords, so the
|
||||
/// output stays proportional to the input vertices. Each arc can sample thousands of points,
|
||||
/// so a compact polyline could otherwise expand into gigabytes (CWE-400).
|
||||
pub fn tessellate_within(&self, tol: f64, budget: &mut usize) -> Vec<Vec2> {
|
||||
let mut out: Vec<Vec2> = Vec::new();
|
||||
for s in self.segments() {
|
||||
let mut pts = Vec::new();
|
||||
if *budget > 0 {
|
||||
s.tessellate(tol, &mut pts);
|
||||
*budget = budget.saturating_sub(pts.len());
|
||||
} else {
|
||||
pts.extend([s.start(), s.end()]);
|
||||
}
|
||||
s.tessellate(tol, &mut pts);
|
||||
let skip = usize::from(out.last().is_some_and(|l| pts.first().is_some_and(|f| l.near(*f, 1e-9))));
|
||||
out.extend(pts.into_iter().skip(skip));
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ mod vec;
|
||||
|
||||
pub use bounds::Bounds2;
|
||||
pub use curve::{
|
||||
Arc, Circle, Ellipse, Line, MAX_TESSELLATED_POINTS, PolyVertex, Polyline, Segment, arc_to_bulge, bulge_to_arc, point_in_polygon,
|
||||
polygon_centroid, shoelace,
|
||||
Arc, Circle, Ellipse, Line, PolyVertex, Polyline, Segment, arc_to_bulge, bulge_to_arc, point_in_polygon, polygon_centroid, shoelace,
|
||||
};
|
||||
pub use fillet::{CornerFit, TanCurve, tangent_circles};
|
||||
pub use intersect::{circle_circle, intersect_ext, intersect_segments, line_circle, line_line, line_line_infinite};
|
||||
|
||||
@@ -221,18 +221,6 @@ fn segment_offset() {
|
||||
assert!(s.offset(6.0).is_none());
|
||||
}
|
||||
|
||||
/// A polyline of many fine arcs samples at most the point budget, then draws chords.
|
||||
#[test]
|
||||
fn polyline_tessellation_is_bounded() {
|
||||
let vertices: Vec<PolyVertex> = (0..3000).map(|i| PolyVertex { bulge: 1.0, ..PolyVertex::new(Vec2::new(f64::from(i) * 1e6, 0.0)) }).collect();
|
||||
let pl = Polyline { vertices, closed: false };
|
||||
let pts = pl.tessellate(1e-9);
|
||||
assert!(pts.len() <= MAX_TESSELLATED_POINTS + 2 * 3000 + 4096, "{}", pts.len());
|
||||
assert!(pts.len() > 3000);
|
||||
let mut budget = 10;
|
||||
assert!(pl.tessellate_within(1e-9, &mut budget).len() <= 4096 + 2 * 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_segments_bounded() {
|
||||
assert_eq!(arc_segments(0.0, 1.0, 0.1), 1);
|
||||
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
cadcraft-geom = { workspace = true }
|
||||
cadcraft-color = { workspace = true }
|
||||
cadcraft-doc = { workspace = true }
|
||||
cadcraft-dxf = { workspace = true }
|
||||
craft-dxf = { workspace = true }
|
||||
cadcraft-dwg = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
cadcraft-fonts = { workspace = true }
|
||||
|
||||
@@ -13,8 +13,8 @@ use std::collections::HashMap;
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{ArrayParams, AssocSnap, Constraint, DimAssoc, DimStyle, Handle, LayerState, Parametric, PlotStyleTable};
|
||||
use cadcraft_dxf::{Tag, Version};
|
||||
use cadcraft_render::Arrowhead;
|
||||
use craft_dxf::{Tag, Version};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, EntityStore, Handle, Image};
|
||||
use cadcraft_dxf::{Section, Tag};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use craft_dxf::{Section, Tag};
|
||||
|
||||
/// Most clip boundary vertices kept per image (hostile files).
|
||||
const MAX_CLIP: usize = 100_000;
|
||||
|
||||
@@ -13,8 +13,8 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Attrib, Drawing, Entity, EntityKind, Handle, Insert, MLeader, MLeaderStyle, MText};
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::Vec3;
|
||||
use craft_dxf::Tag;
|
||||
|
||||
/// Most leader roots (landings) read per multileader.
|
||||
const MAX_ROOTS: usize = 256;
|
||||
|
||||
@@ -12,8 +12,8 @@ use cadcraft_doc::{
|
||||
Drawing, Entity, EntityKind, EntityStore, MAX_MLINE_CUTS, MAX_MLINE_ELEMENTS, MAX_MLINE_VERTICES, MLine, MLineCaps, MLineCut, MLineElement,
|
||||
MLineJust, MLineStyle, MLineVertex, STANDARD_MLINE_STYLE,
|
||||
};
|
||||
use cadcraft_dxf::{Section, Tag};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use craft_dxf::{Section, Tag};
|
||||
|
||||
/// Most groups 41 kept per element and vertex (start, then break pairs).
|
||||
const MAX_PARAMS: usize = 2 * MAX_MLINE_CUTS + 2;
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::foreign::{Foreign, ForeignExtra, ForeignKey, ForeignObject, class_group, class_index, class_number};
|
||||
use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, RawTag, Unknown};
|
||||
use cadcraft_dxf::{Section, Tag};
|
||||
use craft_dxf::{Section, Tag};
|
||||
|
||||
/// Named object dictionary entries CADCraft writes itself from its model (plus every key that
|
||||
/// starts with `CADCRAFT`): they are read into the model, not kept raw. A change that starts
|
||||
@@ -71,7 +71,7 @@ fn raw(t: &Tag) -> RawTag {
|
||||
/// A kept group with the value type its code has (so binary output encodes it right); text that
|
||||
/// doesn't parse as that type stays text.
|
||||
fn tag(r: &RawTag) -> Tag {
|
||||
use cadcraft_dxf::{Kind, Value, kind_of};
|
||||
use craft_dxf::{Kind, Value, kind_of};
|
||||
let v = r.value.trim();
|
||||
let value = match kind_of(r.code) {
|
||||
Kind::Real => v.parse::<f64>().ok().filter(|x| x.is_finite()).map(Value::Real),
|
||||
|
||||
@@ -9,8 +9,8 @@ use cadcraft_doc::{
|
||||
Linetype, Lineweight, LwPolyline, MAX_FRAME_CHARS, MLeaderStyle, MText, MultilineAttrib, NamedView, PageSetup, Point, Polyline3d, RawTag,
|
||||
RayLine, Solid, Table, TableCell, Text, TextStyle, Tolerance, Transparency, Ucs, Unknown, VAlign, Viewport, VportConfig, VportTile, Wipeout,
|
||||
};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::{PolyVertex, Spline, Vec2, Vec3};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
use crate::{IoError, Result};
|
||||
|
||||
@@ -84,7 +84,7 @@ type LayoutDef = (String, u32, String, PageSetup, Option<(Vec2, Vec2)>);
|
||||
|
||||
/// Import DXF sections while sharing one materialized-table-cell budget across blocks and entity sections.
|
||||
fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?;
|
||||
let tags = craft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?;
|
||||
let secs = sections(&tags);
|
||||
if secs.is_empty() {
|
||||
return Err(IoError::Format("no sections found".into()));
|
||||
|
||||
@@ -420,7 +420,7 @@ pub(super) fn acad_table(tags: &[Tag], table_cells_left: &mut usize) -> Table {
|
||||
|
||||
/// The plot style names of a file: the entries of the `ACAD_PLOTSTYLENAME` dictionary of the
|
||||
/// named object dictionary (the first object), by placeholder handle.
|
||||
pub(super) fn plot_style_names(secs: &[cadcraft_dxf::Section]) -> HashMap<String, String> {
|
||||
pub(super) fn plot_style_names(secs: &[craft_dxf::Section]) -> HashMap<String, String> {
|
||||
let mut out = HashMap::new();
|
||||
let Some(objects) = secs.iter().find(|s| s.name == "OBJECTS") else { return out };
|
||||
let recs = records(&objects.tags);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! `^2` in `10^2` as a control character.
|
||||
|
||||
use cadcraft_doc::{MText, MTextColumnType, MTextColumns};
|
||||
use cadcraft_dxf::Tag;
|
||||
use craft_dxf::Tag;
|
||||
|
||||
/// Upper bound on column heights read from a file.
|
||||
const MAX_COLUMNS: usize = 1_000;
|
||||
|
||||
@@ -33,9 +33,9 @@ pub(super) fn header(tags: &[Tag], d: &mut Drawing) {
|
||||
[x, y, z] if x.code == 10 => HVal::Point(Vec3::new(x.f64(), y.f64(), z.f64())),
|
||||
[x, y] if x.code == 10 => HVal::Point(Vec3::new(x.f64(), y.f64(), 0.0)),
|
||||
[v] => match &v.value {
|
||||
cadcraft_dxf::Value::Real(r) => HVal::Real(*r),
|
||||
cadcraft_dxf::Value::Int(n) => HVal::Int(*n),
|
||||
cadcraft_dxf::Value::Bool(b) => HVal::Int(i64::from(*b)),
|
||||
craft_dxf::Value::Real(r) => HVal::Real(*r),
|
||||
craft_dxf::Value::Int(n) => HVal::Int(*n),
|
||||
craft_dxf::Value::Bool(b) => HVal::Int(i64::from(*b)),
|
||||
other => HVal::Str(other.as_str()),
|
||||
},
|
||||
_ => continue,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! kept as unknown objects.
|
||||
|
||||
use cadcraft_doc::{EntityKind, HatchLoop, LwPolyline, MAX_REGION_LOOPS, Region};
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::{PolyVertex, Vec2};
|
||||
use craft_dxf::Tag;
|
||||
|
||||
use crate::dxf_ext::{APP, xdata, xdata_list};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{CellBorder, CellStyle, DataLink, Lineweight, MAX_CELL_STYLES, MAX_TABLE_PARTS, Table, TableBreak, TableStyle};
|
||||
use cadcraft_dxf::Tag;
|
||||
use craft_dxf::Tag;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dxf_ext::{APP, xdata};
|
||||
|
||||
@@ -11,8 +11,8 @@ use cadcraft_doc::{
|
||||
HVal, Handle, Hatch, Insert, Layout, Leader, Line, Lineweight, LwPolyline, MLeader, MLeaderStyle, MText, PageSetup, Point, Solid, Space, Table,
|
||||
TableStyle, Text, Transparency, VAlign, VportConfig, VportTile,
|
||||
};
|
||||
use cadcraft_dxf::{Tag, Version};
|
||||
use cadcraft_geom::{Bounds2, Vec2, Vec3};
|
||||
use craft_dxf::{Tag, Version};
|
||||
|
||||
use crate::dxf_ext::{self, DimVal, K};
|
||||
|
||||
@@ -111,7 +111,7 @@ impl W {
|
||||
/// Write a drawing as ASCII DXF.
|
||||
/// The drawing as R2000 ASCII DXF.
|
||||
pub fn write(d: &Drawing) -> String {
|
||||
cadcraft_dxf::write_ascii(&tags(d, Version::R2000))
|
||||
craft_dxf::write_ascii(&tags(d, Version::R2000))
|
||||
}
|
||||
|
||||
/// The drawing's DXF tags, declaring `version` in `$ACADVER`.
|
||||
@@ -124,7 +124,7 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
|
||||
pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> String {
|
||||
let mut tags = tags_for(d, Version::R2000, dwg, false);
|
||||
bridge_markers(&mut tags);
|
||||
cadcraft_dxf::write_ascii(&tags)
|
||||
craft_dxf::write_ascii(&tags)
|
||||
}
|
||||
|
||||
/// Proxy type names and lookup-parameter subclass names must match the bridge reader's spelling.
|
||||
|
||||
@@ -144,6 +144,6 @@ pub(super) fn header_vars(w: &mut W, d: &Drawing) {
|
||||
/// from 2007 (UTF-8 text), `ANSI_1252`.
|
||||
pub(crate) fn code_page(d: &Drawing, version: Version) -> String {
|
||||
let read = d.header.get("DWGCODEPAGE").and_then(HVal::as_str).map(str::trim).unwrap_or_default();
|
||||
let known = read.is_ascii() && read.len() <= 32 && cadcraft_dxf::decode_code_page(b"", read).is_some();
|
||||
let known = read.is_ascii() && read.len() <= 32 && craft_dxf::decode_code_page(b"", read).is_some();
|
||||
if version < Version::R2007 && known { read.to_string() } else { "ANSI_1252".into() }
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use cadcraft_geom::Bounds2;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use cadcraft_doc::Format;
|
||||
pub use cadcraft_dxf::Version;
|
||||
pub use craft_dxf::Version;
|
||||
pub use dxf_read::entity_from_groups;
|
||||
pub use dxf_write::entity_groups;
|
||||
pub use pdf::{PdfOptions, pdf, plot};
|
||||
@@ -114,10 +114,10 @@ pub fn write_with(d: &Drawing, name: impl AsRef<Path>, opts: &SaveOptions) -> Re
|
||||
// Before 2007 the text is in the code page the header declares.
|
||||
let code_page = dxf_write::code_page(d, version);
|
||||
Ok(match (opts.binary, version.utf8()) {
|
||||
(true, true) => cadcraft_dxf::write_binary(&tags, true),
|
||||
(true, false) => cadcraft_dxf::write_binary_in(&tags, &code_page),
|
||||
(false, true) => cadcraft_dxf::write_ascii_as(&tags, true).into_bytes(),
|
||||
(false, false) => cadcraft_dxf::write_ascii_in(&tags, &code_page),
|
||||
(true, true) => craft_dxf::write_binary(&tags, true),
|
||||
(true, false) => craft_dxf::write_binary_in(&tags, &code_page),
|
||||
(false, true) => craft_dxf::write_ascii_as(&tags, true).into_bytes(),
|
||||
(false, false) => craft_dxf::write_ascii_in(&tags, &code_page),
|
||||
})
|
||||
}
|
||||
// The DWG bridge reads our R2000 DXF and writes the DWG at the version asked for. Without
|
||||
|
||||
@@ -39,7 +39,7 @@ fn bump(m: &mut BTreeMap<String, usize>, k: &str) {
|
||||
|
||||
/// A layout's own paper-space viewport (VIEWPORT with ID 1, group 69): part of the layout, not
|
||||
/// an entity of its own for CADCraft.
|
||||
fn is_overall_viewport(t: &str, g: &[cadcraft_dxf::Tag]) -> bool {
|
||||
fn is_overall_viewport(t: &str, g: &[craft_dxf::Tag]) -> bool {
|
||||
t == "VIEWPORT" && g.iter().any(|x| x.code == 69 && x.i64() == 1)
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ fn is_space_block(name: &str) -> bool {
|
||||
|
||||
/// Count what a DXF file (ASCII or binary) holds; `None` when it can't be parsed.
|
||||
pub fn census(dxf: &[u8]) -> Option<Census> {
|
||||
let tags = cadcraft_dxf::parse(dxf).ok()?;
|
||||
let tags = craft_dxf::parse(dxf).ok()?;
|
||||
let mut c = Census::default();
|
||||
// Blocks CADCraft generates (tables' blocks, standard arrowheads nothing inserts) are left out
|
||||
// on read and rebuilt on save: counted apart.
|
||||
let mut table_blocks: HashSet<String> = HashSet::new();
|
||||
let mut inserted: HashSet<String> = HashSet::new();
|
||||
let mut blocks: Vec<(String, BTreeMap<String, usize>)> = Vec::new();
|
||||
let mut note_refs = |t: &str, g: &[cadcraft_dxf::Tag]| {
|
||||
let mut note_refs = |t: &str, g: &[craft_dxf::Tag]| {
|
||||
let name = || g.iter().find(|x| x.code == 2).map(|x| x.str().trim().to_ascii_uppercase());
|
||||
match t {
|
||||
"ACAD_TABLE" => table_blocks.extend(name()),
|
||||
@@ -65,8 +65,8 @@ pub fn census(dxf: &[u8]) -> Option<Census> {
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
for s in cadcraft_dxf::sections(&tags) {
|
||||
let records = cadcraft_dxf::records(&s.tags);
|
||||
for s in craft_dxf::sections(&tags) {
|
||||
let records = craft_dxf::records(&s.tags);
|
||||
match s.name.trim().to_ascii_uppercase().as_str() {
|
||||
"ENTITIES" => {
|
||||
for (t, g) in &records {
|
||||
@@ -82,7 +82,7 @@ pub fn census(dxf: &[u8]) -> Option<Census> {
|
||||
note_refs(t, g);
|
||||
match t.as_str() {
|
||||
"BLOCK" => {
|
||||
let name = g.iter().find(|x| x.code == 2).map(cadcraft_dxf::Tag::str).unwrap_or_default();
|
||||
let name = g.iter().find(|x| x.code == 2).map(craft_dxf::Tag::str).unwrap_or_default();
|
||||
space = is_space_block(&name);
|
||||
if !space {
|
||||
blocks.push((name.trim().to_ascii_uppercase(), BTreeMap::new()));
|
||||
|
||||
@@ -1483,7 +1483,7 @@ fn ac1024_dwg_with_many_entities_opens() {
|
||||
let n = 30_000;
|
||||
let dwg = ac1024_dwg(n);
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(&dwg).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&dxf).unwrap().len();
|
||||
let tags = craft_dxf::parse(&dxf).unwrap().len();
|
||||
assert!(dxf.len() < dwg.len() * 20, "DXF {} bytes from {} DWG bytes", dxf.len(), dwg.len());
|
||||
assert!(tags < dwg.len(), "{tags} group codes from {} DWG bytes", dwg.len());
|
||||
let d = read(&dwg, "big.dwg").unwrap();
|
||||
@@ -1512,7 +1512,7 @@ fn oversized_dwg_and_dxf_are_rejected_early() {
|
||||
|
||||
// Lines that aren't even group codes: the size is reported before anything is parsed.
|
||||
let text = "not a group code\nx\n".repeat(60);
|
||||
let e = cadcraft_dxf::parse_with_limit(text.as_bytes(), 50).unwrap_err();
|
||||
let e = craft_dxf::parse_with_limit(text.as_bytes(), 50).unwrap_err();
|
||||
assert_eq!(e.to_string(), "drawing too large: 60 DXF group codes, more than the 50 CADCraft reads (set CADCRAFT_MAX_DXF_TAGS to read more)");
|
||||
}
|
||||
|
||||
@@ -1608,7 +1608,7 @@ fn dimension_associativity_roundtrips() {
|
||||
|
||||
// Without CADCraft's xdata (a file from another writer), the standard DIMASSOC objects
|
||||
// still give the extension-line links.
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let mut stripped = Vec::new();
|
||||
let mut skipping = false;
|
||||
for t in tags {
|
||||
@@ -1621,7 +1621,7 @@ fn dimension_associativity_roundtrips() {
|
||||
stripped.push(t);
|
||||
}
|
||||
}
|
||||
let foreign = read_dxf(cadcraft_dxf::write_ascii(&stripped).as_bytes()).unwrap();
|
||||
let foreign = read_dxf(craft_dxf::write_ascii(&stripped).as_bytes()).unwrap();
|
||||
assert_eq!(assoc_of(&foreign, hl), lin.assoc);
|
||||
let fa = assoc_of(&foreign, ha);
|
||||
assert_eq!(fa.len(), 2);
|
||||
@@ -1697,7 +1697,7 @@ fn polyline_vertex_dimensions_round_trip_by_segment() {
|
||||
];
|
||||
let hp = d.add(&Space::Model, c(), EntityKind::Dimension(on_pl.clone())).unwrap();
|
||||
let text = write_dxf(&d);
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(text.as_bytes()).unwrap();
|
||||
// Each reference to the polyline: (72, 73, 91).
|
||||
let refs: Vec<(i64, i64, i64)> = tags
|
||||
.iter()
|
||||
@@ -1726,13 +1726,13 @@ fn polyline_vertex_dimensions_round_trip_by_segment() {
|
||||
in_ref = t.str() == "AcDbOsnapPointRef";
|
||||
}
|
||||
if in_ref && matches!(t.code, 10 | 20) {
|
||||
t = cadcraft_dxf::Tag::new(t.code, cadcraft_dxf::Value::Real(0.0));
|
||||
t = craft_dxf::Tag::new(t.code, craft_dxf::Value::Real(0.0));
|
||||
}
|
||||
if !skipping {
|
||||
stripped.push(t);
|
||||
}
|
||||
}
|
||||
let foreign = read_dxf(cadcraft_dxf::write_ascii(&stripped).as_bytes()).unwrap();
|
||||
let foreign = read_dxf(craft_dxf::write_ascii(&stripped).as_bytes()).unwrap();
|
||||
match &foreign.entity(hp).unwrap().kind {
|
||||
EntityKind::Dimension(x) => assert_eq!(x.assoc, on_pl.assoc),
|
||||
_ => panic!(),
|
||||
@@ -2041,7 +2041,7 @@ fn non_ascii_text_is_escaped_for_r2000_and_roundtrips() {
|
||||
let d = intl_sample();
|
||||
let text = write_dxf(&d);
|
||||
assert!(text.is_ascii(), "an AC1015 DXF must not contain UTF-8 bytes");
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let header = |name: &str| tags.windows(2).find(|w| w[0].code == 9 && w[0].str() == name).map(|w| w[1].str());
|
||||
assert_eq!(header("$ACADVER").as_deref(), Some("AC1015"));
|
||||
assert_eq!(header("$DWGCODEPAGE").as_deref(), Some("ANSI_1252"));
|
||||
|
||||
@@ -173,16 +173,16 @@ fn cyclic_hard_owners_with_a_live_holder_stay_kept() {
|
||||
let bytes = write(&drawing, name).unwrap();
|
||||
drawing = read(&bytes, name).unwrap();
|
||||
let text = write_dxf(&drawing);
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let objects = cadcraft_dxf::sections(&tags).into_iter().find(|s| s.name == "OBJECTS").unwrap();
|
||||
let records = cadcraft_dxf::records(&objects.tags);
|
||||
let tags = craft_dxf::parse(text.as_bytes()).unwrap();
|
||||
let objects = craft_dxf::sections(&tags).into_iter().find(|s| s.name == "OBJECTS").unwrap();
|
||||
let records = craft_dxf::records(&objects.tags);
|
||||
let record = |label: &str| records.iter().find(|(_, tags)| tags.iter().any(|t| t.code == 1 && t.str() == label)).unwrap().1.clone();
|
||||
let first = record("first");
|
||||
let second = record("second");
|
||||
let holder = record("holder");
|
||||
let child = record("child");
|
||||
assert!(!records.iter().any(|(_, tags)| tags.iter().any(|t| t.code == 1 && t.str() == "stray")), "{name}");
|
||||
let handle = |tags: &[cadcraft_dxf::Tag]| tags.iter().find(|t| t.code == 5).unwrap().str();
|
||||
let handle = |tags: &[craft_dxf::Tag]| tags.iter().find(|t| t.code == 5).unwrap().str();
|
||||
assert!(first.iter().any(|t| t.code == 360 && t.str() == handle(&second)), "{name}");
|
||||
assert!(second.iter().any(|t| t.code == 360 && t.str() == handle(&first)), "{name}");
|
||||
assert!(first.iter().any(|t| t.code == 360 && t.str() == handle(&child)), "{name}");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Snap/grid spacing uses legacy HEADER variables and the current *ACTIVE VPORT (#607).
|
||||
|
||||
use cadcraft_doc::{Drawing, HVal};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
fn header(snap: Vec2, grid: Vec2) -> Vec<Tag> {
|
||||
let mut tags = Vec::new();
|
||||
@@ -24,7 +24,7 @@ fn read(header: &[Tag], vports: &[Vec<Tag>]) -> Drawing {
|
||||
tags.extend_from_slice(vp);
|
||||
}
|
||||
tags.extend([Tag::s(0, "ENDTAB"), Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]);
|
||||
cadcraft_io::read_dxf(cadcraft_dxf::write_ascii(&tags).as_bytes()).unwrap()
|
||||
cadcraft_io::read_dxf(craft_dxf::write_ascii(&tags).as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
fn assert_units(d: &Drawing, snap: Vec2, grid: Vec2) {
|
||||
@@ -43,7 +43,7 @@ fn writes_spacing_to_header_and_active_vport_and_roundtrips() {
|
||||
d.header.set("SNAPUNIT", HVal::Point(snap.to3(0.0)));
|
||||
d.header.set("GRIDUNIT", HVal::Point(grid.to3(0.0)));
|
||||
let text = cadcraft_io::write_dxf(&d);
|
||||
let tags = cadcraft_dxf::parse_ascii(&text).unwrap();
|
||||
let tags = craft_dxf::parse_ascii(&text).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let header = §ions.iter().find(|s| s.name == "HEADER").unwrap().tags;
|
||||
for (name, unit) in [("$SNAPUNIT", snap), ("$GRIDUNIT", grid)] {
|
||||
|
||||
@@ -62,7 +62,7 @@ fn opts(version: Version, binary: bool) -> SaveOptions {
|
||||
}
|
||||
|
||||
fn acadver(bytes: &[u8]) -> String {
|
||||
let tags = cadcraft_dxf::parse(bytes).unwrap();
|
||||
let tags = craft_dxf::parse(bytes).unwrap();
|
||||
tags.windows(2).find(|w| w[0].code == 9 && w[0].str() == "$ACADVER").map(|w| w[1].str()).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ fn dxf_at_every_version_reads_back_the_same() {
|
||||
let bytes = write_with(&d, "x.dxf", &opts(v, binary)).unwrap();
|
||||
let what = format!("{v} binary={binary}");
|
||||
assert_eq!(acadver(&bytes), v.code(), "{what}");
|
||||
assert_eq!(cadcraft_dxf::is_binary(&bytes), binary, "{what}");
|
||||
assert_eq!(craft_dxf::is_binary(&bytes), binary, "{what}");
|
||||
let raw = bytes.windows("图号".len()).any(|w| w == "图号".as_bytes());
|
||||
assert_eq!(raw, v.utf8(), "{what}: UTF-8 text from 2007, the code page before");
|
||||
if !v.utf8() {
|
||||
@@ -109,11 +109,11 @@ fn class_records_follow_the_version() {
|
||||
let d = sample();
|
||||
for v in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let tags = cadcraft_dxf::parse(&write_with(&d, "x.dxf", &opts(v, binary)).unwrap()).unwrap();
|
||||
let classes: Vec<Vec<i32>> = cadcraft_dxf::sections(&tags)
|
||||
let tags = craft_dxf::parse(&write_with(&d, "x.dxf", &opts(v, binary)).unwrap()).unwrap();
|
||||
let classes: Vec<Vec<i32>> = craft_dxf::sections(&tags)
|
||||
.into_iter()
|
||||
.filter(|s| s.name == "CLASSES")
|
||||
.flat_map(|s| cadcraft_dxf::records(&s.tags))
|
||||
.flat_map(|s| craft_dxf::records(&s.tags))
|
||||
.filter(|(kind, _)| kind == "CLASS")
|
||||
.map(|(_, t)| t.iter().map(|x| x.code).collect())
|
||||
.collect();
|
||||
@@ -134,7 +134,7 @@ fn r2000_ascii_is_unchanged() {
|
||||
let saved = write_with(&d, "x.dxf", &opts(Version::R2000, false)).unwrap();
|
||||
let escaped = write_dxf(&d);
|
||||
assert_ne!(saved, escaped.as_bytes(), "Café is written in ANSI_1252");
|
||||
assert_eq!(cadcraft_dxf::parse(&saved).unwrap(), cadcraft_dxf::parse(escaped.as_bytes()).unwrap());
|
||||
assert_eq!(craft_dxf::parse(&saved).unwrap(), craft_dxf::parse(escaped.as_bytes()).unwrap());
|
||||
}
|
||||
|
||||
/// Without a version asked for: 2018 for a new drawing, otherwise the file's own version (the
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! don't panic.
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Helix, HelixConstrain, Space};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::Vec3;
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
fn helices(d: &Drawing) -> Vec<Helix> {
|
||||
d.model
|
||||
@@ -65,7 +65,7 @@ fn helix_survives_save_and_reopen() {
|
||||
assert_eq!(back.model.iter().next().unwrap().common.layer, "Springs");
|
||||
|
||||
// The file: a spline whose last control point is the helix's end, the helix data, a CLASS.
|
||||
let tags = cadcraft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let secs = sections(&tags);
|
||||
let recs = |name: &str| records(&secs.iter().find(|s| s.name == name).unwrap().tags);
|
||||
let ents = recs("ENTITIES");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Common, DimStyle, Drawing, EntityKind, Handle, Leader, Lineweight, Space};
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::Tag;
|
||||
|
||||
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
@@ -93,7 +93,7 @@ fn missing_z_defaults_only_the_corresponding_vertex() -> TestResult {
|
||||
expected.push(Vec3::new(p.x, p.y, if has_z { p.z } else { 0.0 }));
|
||||
}
|
||||
tags.extend([Tag::s(0, "ENDSEC"), Tag::s(0, "EOF")]);
|
||||
let dxf = cadcraft_dxf::write_ascii(&tags);
|
||||
let dxf = craft_dxf::write_ascii(&tags);
|
||||
let d = read(dxf.as_bytes(), "missing-z.dxf")?;
|
||||
let e = d.entity(Handle(0x10)).ok_or("LEADER missing")?;
|
||||
let EntityKind::Leader(l) = &e.kind else { return Err("expected LEADER".into()) };
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, MLine, MLineCaps, MLineCut, MLineElement, MLineJust, MLineStyle, Space};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::Vec2;
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
fn mlines(d: &Drawing) -> Vec<MLine> {
|
||||
d.model
|
||||
@@ -84,7 +84,7 @@ fn mlines_and_styles_survive_save_and_reopen() {
|
||||
assert_eq!(back.header.f64("CMLSCALE", 0.0), 2.5);
|
||||
|
||||
// The file links up as the DXF Reference describes.
|
||||
let tags = cadcraft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let secs = sections(&tags);
|
||||
let recs = |name: &str| records(&secs.iter().find(|s| s.name == name).unwrap().tags);
|
||||
let val = |t: &[Tag], c: i32| t.iter().find(|g| g.code == c).map(Tag::str).unwrap_or_default();
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//! 75/76/78/79/48/49/50, tabs as `^I`, carets as `^ `.
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, MText, MTextColumnType, MTextColumns, Space};
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, entity_from_groups, read_dxf, write_dxf, write_with};
|
||||
use craft_dxf::Tag;
|
||||
|
||||
fn mtext(contents: &str, rotation: f64, columns: Option<MTextColumns>) -> MText {
|
||||
MText {
|
||||
@@ -246,8 +246,8 @@ fn markers_inside_an_application_group_do_not_end_the_entity_groups() {
|
||||
|
||||
// ---- #2248: R2018 embedded column object ----
|
||||
|
||||
fn embedded_tags(count: i64, width: f64, gutter: f64, heights: &[f64]) -> Vec<cadcraft_dxf::Tag> {
|
||||
use cadcraft_dxf::Tag;
|
||||
fn embedded_tags(count: i64, width: f64, gutter: f64, heights: &[f64]) -> Vec<craft_dxf::Tag> {
|
||||
use craft_dxf::Tag;
|
||||
let mut v = vec![
|
||||
Tag::s(8, "0"),
|
||||
Tag::f(10, 10.0),
|
||||
@@ -281,7 +281,7 @@ fn embedded_tags(count: i64, width: f64, gutter: f64, heights: &[f64]) -> Vec<ca
|
||||
v
|
||||
}
|
||||
|
||||
fn mtext_from_groups(tags: &[cadcraft_dxf::Tag]) -> (Common, MText) {
|
||||
fn mtext_from_groups(tags: &[craft_dxf::Tag]) -> (Common, MText) {
|
||||
let Some((common, EntityKind::MText(t))) = entity_from_groups("MTEXT", tags) else { panic!("MTEXT was not read") };
|
||||
(common, t)
|
||||
}
|
||||
@@ -322,7 +322,7 @@ fn embedded_column_height_is_the_entitys_defined_height() {
|
||||
// As ezdxf and AutoCAD write static columns: the height in the entity's 46, none in the object.
|
||||
let mut tags = embedded_tags(3, 12.5, 2.5, &[]);
|
||||
let at = tags.iter().position(|t| t.code == 101).unwrap();
|
||||
tags.insert(at, cadcraft_dxf::Tag::f(46, 20.0));
|
||||
tags.insert(at, craft_dxf::Tag::f(46, 20.0));
|
||||
let (_, t) = mtext_from_groups(&tags);
|
||||
assert_eq!(t.columns.map(|c| c.height), Some(20.0));
|
||||
}
|
||||
@@ -355,10 +355,10 @@ fn dynamic_auto_height_embedded_count_comes_from_the_total_width() {
|
||||
let at = tags.iter().position(|t| t.code == 101).unwrap();
|
||||
for t in &mut tags[at..] {
|
||||
*t = match t.code {
|
||||
42 => cadcraft_dxf::Tag::f(42, 175.0),
|
||||
71 => cadcraft_dxf::Tag::i(71, 2),
|
||||
72 => cadcraft_dxf::Tag::i(72, 0),
|
||||
73 => cadcraft_dxf::Tag::i(73, 1),
|
||||
42 => craft_dxf::Tag::f(42, 175.0),
|
||||
71 => craft_dxf::Tag::i(71, 2),
|
||||
72 => craft_dxf::Tag::i(72, 0),
|
||||
73 => craft_dxf::Tag::i(73, 1),
|
||||
_ => t.clone(),
|
||||
};
|
||||
}
|
||||
@@ -373,13 +373,13 @@ fn dynamic_auto_height_count_comes_from_the_widths_when_72_is_zero() {
|
||||
// A dynamic auto-height object stores 0 in 72; the count is implied by the total width
|
||||
// (42 = n * 44 + (n - 1) * 45): (175 + 12.5) / (50 + 12.5) = 3.
|
||||
let mut tags = embedded_tags(0, 50.0, 12.5, &[]);
|
||||
let mut set = |code: i32, v: cadcraft_dxf::Tag| {
|
||||
let mut set = |code: i32, v: craft_dxf::Tag| {
|
||||
let at = tags.iter().rposition(|t| t.code == code).unwrap();
|
||||
tags[at] = v;
|
||||
};
|
||||
set(71, cadcraft_dxf::Tag::i(71, 2));
|
||||
set(73, cadcraft_dxf::Tag::i(73, 1));
|
||||
set(42, cadcraft_dxf::Tag::f(42, 175.0));
|
||||
set(71, craft_dxf::Tag::i(71, 2));
|
||||
set(73, craft_dxf::Tag::i(73, 1));
|
||||
set(42, craft_dxf::Tag::f(42, 175.0));
|
||||
let (_, t) = mtext_from_groups(&tags);
|
||||
let c = t.columns.expect("columns");
|
||||
assert_eq!((c.kind, c.auto_height, c.count), (MTextColumnType::Dynamic, true, 3));
|
||||
@@ -392,10 +392,10 @@ fn embedded_column_width_wins_over_the_entitys_width() {
|
||||
// what the R2018 object gets back (#2467 review).
|
||||
let mut tags = embedded_tags(3, 50.0, 12.5, &[20.0, 20.0, 20.0]);
|
||||
let own_41 = tags.iter().position(|t| t.code == 41).unwrap();
|
||||
tags[own_41] = cadcraft_dxf::Tag::f(41, 175.0);
|
||||
tags[own_41] = craft_dxf::Tag::f(41, 175.0);
|
||||
for code in [40, 42] {
|
||||
let at = tags.iter().rposition(|t| t.code == code).unwrap();
|
||||
tags[at] = cadcraft_dxf::Tag::f(code, 175.0);
|
||||
tags[at] = craft_dxf::Tag::f(code, 175.0);
|
||||
}
|
||||
let (_, t) = mtext_from_groups(&tags);
|
||||
assert_eq!(t.width, 50.0, "the column width, not the total");
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! RASTERVARIABLES and dictionaries the DXF Reference describes.
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Image};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::Vec2;
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
/// A small DXF as another program writes it: a line, an image on layer `Pics` pointing at an
|
||||
/// IMAGEDEF named `logo`, and an MLINE without vertices.
|
||||
@@ -114,7 +114,7 @@ fn images(d: &Drawing) -> Vec<Image> {
|
||||
|
||||
/// The records of a section of a DXF text.
|
||||
fn section(dxf: &str, name: &str) -> Vec<(String, Vec<Tag>)> {
|
||||
let tags = cadcraft_dxf::parse(dxf.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(dxf.as_bytes()).unwrap();
|
||||
let s = sections(&tags).into_iter().find(|s| s.name == name).unwrap();
|
||||
records(&s.tags)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ fn fonts(d: &Drawing) -> Vec<(String, String)> {
|
||||
fn style_files(dxf: &str) -> Vec<(String, String)> {
|
||||
let mut out: Vec<(String, String)> = Vec::new();
|
||||
let mut style = false;
|
||||
for t in cadcraft_dxf::parse(dxf.as_bytes()).unwrap() {
|
||||
for t in craft_dxf::parse(dxf.as_bytes()).unwrap() {
|
||||
match t.code {
|
||||
0 => style = t.str() == "STYLE",
|
||||
2 if style => out.push((t.str(), String::new())),
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! Foreign REGIONs stay unknown objects as before; hostile xdata doesn't panic.
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, HatchLoop, Region, Space};
|
||||
use cadcraft_dxf::{records, sections};
|
||||
use cadcraft_geom::{PolyVertex, Vec2};
|
||||
use craft_dxf::{records, sections};
|
||||
|
||||
fn regions(d: &Drawing) -> Vec<Region> {
|
||||
d.model
|
||||
@@ -56,7 +56,7 @@ fn region_with_islands_survives_save_and_reopen() {
|
||||
assert!(matches!(back.entity(plain).unwrap().kind, EntityKind::LwPolyline(_)));
|
||||
|
||||
// Other readers see four closed polylines on the region's layer.
|
||||
let tags = cadcraft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let tags = craft_dxf::parse(saved.as_bytes()).unwrap();
|
||||
let ents = records(§ions(&tags).into_iter().find(|s| s.name == "ENTITIES").unwrap().tags);
|
||||
let polys: Vec<_> = ents.iter().filter(|(k, _)| k == "LWPOLYLINE").collect();
|
||||
assert_eq!(polys.len(), 5);
|
||||
|
||||
@@ -47,7 +47,7 @@ fn edit_primary(drawing: &mut Drawing, edit: impl Fn(&mut Attrib)) {
|
||||
fn reopen(drawing: &Drawing, version: Version, binary: bool, context: &str) -> Drawing {
|
||||
let options = SaveOptions { version: Some(version), binary };
|
||||
let bytes = cadcraft_io::write_with(drawing, "attributes.dxf", &options).unwrap_or_else(|e| panic!("{context}: write: {e}"));
|
||||
assert_eq!(cadcraft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
assert_eq!(craft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
let back = cadcraft_io::read(&bytes, "attributes.dxf").unwrap_or_else(|e| panic!("{context}: read: {e}"));
|
||||
assert_eq!(cadcraft_io::save_version(&back), version, "{context}: version");
|
||||
assert!(back.read_notes.is_empty(), "{context}: {:?}", back.read_notes);
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
use cadcraft_color::{Color, Rgb};
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Layer, Lineweight, Transparency};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
fn fixture(properties: &str) -> Drawing {
|
||||
let records = [
|
||||
@@ -45,7 +45,7 @@ fn output(common: &Common, version: Version, binary: bool) -> Vec<(String, Vec<T
|
||||
drawing.model.modify(handle, |e| e.common = common.clone());
|
||||
}
|
||||
let bytes = write_with(&drawing, "memory.dxf", &SaveOptions { version: Some(version), binary }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let entities = records(§ions.iter().find(|s| s.name == "ENTITIES").unwrap().tags);
|
||||
assert_eq!(entities.iter().map(|(kind, _)| kind.as_str()).collect::<Vec<_>>(), ["LINE", "CIRCLE", "TEXT"]);
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Circle, Common, Drawing, Entity, EntityKind, HAlign, Handle, Line, Space, Text, VAlign};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
use super::compare;
|
||||
|
||||
@@ -88,7 +88,7 @@ fn writes_standard_entity_groups_not_just_self_readable_data() {
|
||||
for version in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let bytes = write_with(&model(), "memory.dxf", &SaveOptions { version: Some(version), binary }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let entities = records(§ions.iter().find(|s| s.name == "ENTITIES").unwrap().tags);
|
||||
assert_eq!(entities.len(), expected.len());
|
||||
@@ -161,7 +161,7 @@ fn writes_large_handles_as_exact_hex_groups() {
|
||||
for version in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let bytes = write_with(&drawing, "memory.dxf", &SaveOptions { version: Some(version), binary }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let entities = records(§ions.iter().find(|s| s.name == "ENTITIES").unwrap().tags);
|
||||
assert_eq!(entities.len(), LARGE_HANDLES.len());
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
use super::super::{cases, compare};
|
||||
use cadcraft_doc::{Arc, Common, Ellipse, EntityKind, LwPolyline, MText, Point, RayLine};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_geom::{PolyVertex, Vec2, Vec3};
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
struct Fixture {
|
||||
wire_type: &'static str,
|
||||
@@ -121,7 +121,7 @@ fn writes_extended_entities_with_independent_groups() {
|
||||
for version in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let bytes = write_with(&source.drawing, "memory.dxf", &SaveOptions { version: Some(version), binary }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let records = records(§ions.iter().find(|s| s.name == "ENTITIES").unwrap().tags);
|
||||
assert_eq!(records.len(), 1);
|
||||
|
||||
@@ -39,7 +39,7 @@ fn check_version_with(case: &str, original: &Drawing, expected: &Drawing, versio
|
||||
for pass in 1..=2 {
|
||||
let context = format!("{case} · {version} · binary={binary} · pass={pass}");
|
||||
let bytes = write_with(¤t, "memory.dxf", &options).unwrap_or_else(|e| panic!("{context}: write: {e}"));
|
||||
assert_eq!(cadcraft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
assert_eq!(craft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
let back = read(&bytes, "memory.dxf").unwrap_or_else(|e| panic!("{context}: read: {e}"));
|
||||
assert_eq!(save_version(&back), version, "{context}: version");
|
||||
assert!(back.read_notes.is_empty(), "{context}: read notes: {:?}", back.read_notes);
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Drawing, Layer, TextStyle};
|
||||
use cadcraft_dxf::{Tag, Value, records, sections};
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::{Tag, Value, records, sections};
|
||||
|
||||
fn layers() -> Vec<(Layer, i64)> {
|
||||
let mut cases = Vec::new();
|
||||
@@ -59,7 +59,7 @@ fn fixture(table: &str, body: &str) -> Drawing {
|
||||
|
||||
fn output(drawing: &Drawing, table: &str, version: Version, binary: bool) -> Vec<Tag> {
|
||||
let bytes = write_with(drawing, "memory.dxf", &SaveOptions { version: Some(version), binary }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let tables = records(§ions.iter().find(|s| s.name == "TABLES").unwrap().tags);
|
||||
let matching: Vec<_> = tables.into_iter().filter(|(kind, groups)| kind == table && groups.contains(&Tag::s(2, "Flags"))).collect();
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//! DWG may explicitly refuse an unregistered class; a successful save must not silently drop it.
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, Entity, EntityKind, Line, RawTag, Unknown};
|
||||
use cadcraft_dxf::{records, sections};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
use craft_dxf::{records, sections};
|
||||
|
||||
fn drawing() -> Drawing {
|
||||
let mut d = super::cases::drawing();
|
||||
@@ -55,7 +55,7 @@ fn preserved(original: &Drawing, back: &Drawing, context: &str) {
|
||||
}
|
||||
|
||||
fn dxf_owner(bytes: &[u8]) {
|
||||
let tags = cadcraft_dxf::parse(bytes).unwrap();
|
||||
let tags = craft_dxf::parse(bytes).unwrap();
|
||||
let sections = sections(&tags);
|
||||
let tables = records(§ions.iter().find(|s| s.name == "TABLES").unwrap().tags);
|
||||
let (_, model) = tables
|
||||
@@ -79,7 +79,7 @@ pub(super) fn check(path: &str, version: Version, binary: bool) {
|
||||
Err(error) => panic!("{context}: {error}"),
|
||||
};
|
||||
if path.ends_with(".dxf") {
|
||||
assert_eq!(cadcraft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
assert_eq!(craft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
dxf_owner(&bytes);
|
||||
} else {
|
||||
assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()), "{context}: wire version");
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind};
|
||||
use cadcraft_dxf::{Tag, records, sections};
|
||||
use cadcraft_io::{SaveOptions, Version, read_dxf, write_dxf, write_with};
|
||||
use craft_dxf::{Tag, records, sections};
|
||||
|
||||
/// DXF text from lines of `code value` (the value may be empty).
|
||||
fn dxf(src: &str) -> String {
|
||||
@@ -565,7 +565,7 @@ struct Out {
|
||||
|
||||
impl Out {
|
||||
fn new(text: &str) -> Out {
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).expect("parse written DXF");
|
||||
let tags = craft_dxf::parse(text.as_bytes()).expect("parse written DXF");
|
||||
let mut recs = Vec::new();
|
||||
for s in sections(&tags) {
|
||||
for (k, t) in records(&s.tags) {
|
||||
@@ -898,7 +898,7 @@ fn class_records_have_instance_counts_from_2004_only() {
|
||||
// Binary files the same way: none at R2000, the count at 2018.
|
||||
let acme = |version: Version| {
|
||||
let bytes = write_with(&d, "x.dxf", &SaveOptions { version: Some(version), binary: true }).expect("save binary");
|
||||
let tags = cadcraft_dxf::parse(&bytes).expect("parse binary");
|
||||
let tags = craft_dxf::parse(&bytes).expect("parse binary");
|
||||
let class = sections(&tags)
|
||||
.into_iter()
|
||||
.filter(|sec| sec.name == "CLASSES")
|
||||
|
||||
@@ -76,8 +76,8 @@ fn multileader_styles_and_current_style_round_trip() {
|
||||
d.mleader_styles.push(callout);
|
||||
d.header.set_str("CMLEADERSTYLE", "Callout");
|
||||
let text = write_dxf(&d);
|
||||
let tags = cadcraft_dxf::parse(text.as_bytes()).expect("parse written DXF");
|
||||
let header = cadcraft_dxf::sections(&tags).into_iter().find(|s| s.name == "HEADER").expect("HEADER");
|
||||
let tags = craft_dxf::parse(text.as_bytes()).expect("parse written DXF");
|
||||
let header = craft_dxf::sections(&tags).into_iter().find(|s| s.name == "HEADER").expect("HEADER");
|
||||
assert!(header.tags.iter().all(|t| t.code != 9 || t.str() != "$CMLEADERSTYLE"));
|
||||
let r = read_dxf(text.as_bytes()).expect("reopen");
|
||||
assert_eq!(r.mleader_styles, d.mleader_styles);
|
||||
|
||||
@@ -114,7 +114,7 @@ fn entget_shows_plain_carets_and_entmake_takes_them() {
|
||||
// entmod of entget changes nothing; entmake takes a plain string.
|
||||
let (_, back) = cadcraft_io::entity_from_groups(&kind, &groups).unwrap();
|
||||
assert_eq!(strings(&back), ["x^ y 10^2"]);
|
||||
let made = [cadcraft_dxf::Tag::s(1, "p^ q")];
|
||||
let made = [craft_dxf::Tag::s(1, "p^ q")];
|
||||
assert_eq!(strings(&cadcraft_io::entity_from_groups("TEXT", &made).unwrap().1), ["p^ q"]);
|
||||
// Every kind, and a DIMENSION's style name (also group 3) as it is.
|
||||
let d = drawing();
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Block, ClipBoundary, Common, Drawing, Entity, EntityKind, Handle, Insert, Line, Space};
|
||||
use cadcraft_dxf::Version;
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use cadcraft_io::{SaveOptions, read, write_with};
|
||||
use craft_dxf::Version;
|
||||
|
||||
fn drawing() -> Drawing {
|
||||
let mut d = Drawing::new_metric();
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::sync::Arc;
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_dxf::{Tag, parse_ascii, records, sections, write_ascii};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{read_dxf, write_dxf};
|
||||
use craft_dxf::{Tag, parse_ascii, records, sections, write_ascii};
|
||||
|
||||
fn roundtrip(d: &Drawing) -> Drawing {
|
||||
read_dxf(write_dxf(d).as_bytes()).expect("reopen")
|
||||
|
||||
@@ -46,10 +46,10 @@ fn check(bytes: &[u8], name: &str) -> cadcraft_doc::Drawing {
|
||||
.unwrap();
|
||||
assert!(unknown.tags.iter().any(|t| t.code == 340 && t.value == line.hex()));
|
||||
let dxf = if name.ends_with(".dwg") { cadcraft_dwg::dwg_to_dxf(bytes).unwrap() } else { bytes.to_vec() };
|
||||
let tags = cadcraft_dxf::parse(&dxf).unwrap();
|
||||
let sections = cadcraft_dxf::sections(&tags);
|
||||
let tags = craft_dxf::parse(&dxf).unwrap();
|
||||
let sections = craft_dxf::sections(&tags);
|
||||
let classes = sections.iter().find(|s| s.name == "CLASSES").unwrap();
|
||||
let classes = cadcraft_dxf::records(&classes.tags);
|
||||
let classes = craft_dxf::records(&classes.tags);
|
||||
let decoded = acadrust::DxfReader::from_reader(Cursor::new(dxf)).unwrap().read().unwrap();
|
||||
let proxy = decoded
|
||||
.entities()
|
||||
@@ -119,15 +119,15 @@ fn duplicate_class_dxf() -> Vec<u8> {
|
||||
fn duplicate_names_keep_distinct_classes_for_entities_and_dictionary_objects() {
|
||||
let source = duplicate_class_dxf();
|
||||
for binary in [false, true] {
|
||||
let input = if binary { cadcraft_dxf::write_binary(&cadcraft_dxf::parse(&source).unwrap(), true) } else { source.clone() };
|
||||
let input = if binary { craft_dxf::write_binary(&craft_dxf::parse(&source).unwrap(), true) } else { source.clone() };
|
||||
let mut drawing = cadcraft_io::read(&input, "source.dxf").unwrap();
|
||||
for _ in 0..3 {
|
||||
for name in ["copy.dxf", "copy.dwg"] {
|
||||
let bytes = cadcraft_io::write_with(&drawing, name, &SaveOptions { version: Some(Version::R2018), ..Default::default() }).unwrap();
|
||||
let dxf = if name.ends_with(".dwg") { cadcraft_dwg::dwg_to_dxf(&bytes).unwrap() } else { bytes.clone() };
|
||||
let tags = cadcraft_dxf::parse(&dxf).unwrap();
|
||||
let sections = cadcraft_dxf::sections(&tags);
|
||||
let classes = cadcraft_dxf::records(§ions.iter().find(|s| s.name == "CLASSES").unwrap().tags);
|
||||
let tags = craft_dxf::parse(&dxf).unwrap();
|
||||
let sections = craft_dxf::sections(&tags);
|
||||
let classes = craft_dxf::records(§ions.iter().find(|s| s.name == "CLASSES").unwrap().tags);
|
||||
let custom: Vec<_> = classes.iter().filter(|(_, t)| t.iter().any(|t| t.code == 1 && t.str() == "CUSTOM")).collect();
|
||||
assert_eq!(custom.len(), 3);
|
||||
for (class, (cpp, flags)) in custom.iter().zip([("First", 1025), ("Second", 128), ("Object", 17)]) {
|
||||
@@ -137,7 +137,7 @@ fn duplicate_names_keep_distinct_classes_for_entities_and_dictionary_objects() {
|
||||
assert!(class.1.iter().any(|t| t.code == 280 && t.i64() == 1));
|
||||
}
|
||||
let records: Vec<_> =
|
||||
sections.iter().flat_map(|s| cadcraft_dxf::records(&s.tags)).filter(|(kind, _)| kind.starts_with("ACAD_PROXY_")).collect();
|
||||
sections.iter().flat_map(|s| craft_dxf::records(&s.tags)).filter(|(kind, _)| kind.starts_with("ACAD_PROXY_")).collect();
|
||||
assert_eq!(records.len(), 3);
|
||||
let mut identities = Vec::new();
|
||||
for (_, tags) in &records {
|
||||
@@ -216,16 +216,13 @@ fn proxy_spelling_is_preserved_in_dxf_and_normalized_only_for_dwg() {
|
||||
let drawing = cadcraft_io::read(dxf.as_bytes(), "source.dxf").unwrap();
|
||||
assert_eq!(drawing.entity_count(), 3);
|
||||
let out = cadcraft_io::write_with(&drawing, "copy.dxf", &SaveOptions { version: Some(Version::R2018), ..Default::default() }).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&out).unwrap();
|
||||
let tags = craft_dxf::parse(&out).unwrap();
|
||||
assert!(tags.iter().any(|t| t.code == 0 && t.str() == entity));
|
||||
let bytes = cadcraft_io::write_with(&drawing, "copy.dwg", &SaveOptions { version: Some(Version::R2018), ..Default::default() }).unwrap();
|
||||
let decoded = cadcraft_dwg::dwg_to_dxf(&bytes).unwrap();
|
||||
let tags = cadcraft_dxf::parse(&decoded).unwrap();
|
||||
let proxies: Vec<_> = cadcraft_dxf::sections(&tags)
|
||||
.iter()
|
||||
.flat_map(|s| cadcraft_dxf::records(&s.tags))
|
||||
.filter(|(kind, _)| kind.starts_with("ACAD_PROXY_"))
|
||||
.collect();
|
||||
let tags = craft_dxf::parse(&decoded).unwrap();
|
||||
let proxies: Vec<_> =
|
||||
craft_dxf::sections(&tags).iter().flat_map(|s| craft_dxf::records(&s.tags)).filter(|(kind, _)| kind.starts_with("ACAD_PROXY_")).collect();
|
||||
assert_eq!(proxies.len(), 3);
|
||||
for (_, tags) in proxies {
|
||||
assert!(tags.iter().any(|t| t.code == 310 && t.str() == "A580"));
|
||||
@@ -239,7 +236,7 @@ fn proxy_spelling_is_preserved_in_dxf_and_normalized_only_for_dwg() {
|
||||
/// Exercise the actual ASCII bridge, which writes Unicode as CIF escapes.
|
||||
#[test]
|
||||
fn unicode_class_identity_survives_the_ascii_bridge() {
|
||||
let mut tags = cadcraft_dxf::parse(&duplicate_class_dxf()).unwrap();
|
||||
let mut tags = craft_dxf::parse(&duplicate_class_dxf()).unwrap();
|
||||
for tag in &mut tags {
|
||||
let text = tag.str();
|
||||
let replacement = match (tag.code, text.as_str()) {
|
||||
@@ -251,10 +248,10 @@ fn unicode_class_identity_survives_the_ascii_bridge() {
|
||||
_ => None,
|
||||
};
|
||||
if let Some(text) = replacement {
|
||||
*tag = cadcraft_dxf::Tag::s(tag.code, text);
|
||||
*tag = craft_dxf::Tag::s(tag.code, text);
|
||||
}
|
||||
}
|
||||
let text = cadcraft_dxf::write_ascii(&tags);
|
||||
let text = craft_dxf::write_ascii(&tags);
|
||||
assert!(text.contains("\\U+"));
|
||||
let mut drawing = cadcraft_io::read(text.as_bytes(), "unicode.dxf").unwrap();
|
||||
for _ in 0..2 {
|
||||
|
||||
@@ -21,6 +21,3 @@ getrandom = "0.4"
|
||||
[dev-dependencies]
|
||||
# Reads the pixels of rendered PNGs in the tests.
|
||||
tiny-skia = { workspace = true }
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# O_NOFOLLOW when reading the token file (MIT OR Apache-2.0).
|
||||
libc = "0.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Where MCP tool calls end up: a control-channel method call.
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
@@ -76,17 +76,9 @@ impl Remote {
|
||||
/// The per-session token for this port, read (and cached) from the owner-only token file. A
|
||||
/// missing file is not fatal here: the request is sent without a token and the server replies
|
||||
/// with an authentication error the user can act on.
|
||||
///
|
||||
/// The desktop app listens only on `127.0.0.1:PORT`, so the token goes only to that exact
|
||||
/// peer: a name that also resolves elsewhere (`localhost` → `::1`, where another local account
|
||||
/// may listen on the same port) must not be handed the desktop's credential (CWE-522).
|
||||
fn token(&mut self) -> Option<&str> {
|
||||
let port = port_of(&self.addr)?;
|
||||
let peer = self.conn.as_ref().and_then(|(_, s)| s.peer_addr().ok());
|
||||
if peer != Some(SocketAddr::from((Ipv4Addr::LOCALHOST, port))) {
|
||||
return None;
|
||||
}
|
||||
if self.token.is_none()
|
||||
&& let Some(port) = port_of(&self.addr)
|
||||
&& let Ok(t) = crate::control_token::read(port)
|
||||
{
|
||||
self.token = Some(t);
|
||||
@@ -215,14 +207,6 @@ fn is_read_only(method: &str) -> bool {
|
||||
|
||||
impl Remote {
|
||||
/// Put the current token (if any) on `req`, replacing the one it carries.
|
||||
/// Connect if needed, put the token for the peer actually connected to on `req`, and send it.
|
||||
fn sign_and_send(&mut self, req: &mut Value) -> Result<String, (std::io::Error, bool)> {
|
||||
if self.conn.is_none() {
|
||||
self.reach().map_err(|e| (e, false))?;
|
||||
}
|
||||
self.set_token(req);
|
||||
self.roundtrip(&req.to_string())
|
||||
}
|
||||
fn set_token(&mut self, req: &mut Value) {
|
||||
let token = self.token().map(str::to_string);
|
||||
if let Some(obj) = req.as_object_mut() {
|
||||
@@ -235,7 +219,7 @@ impl Remote {
|
||||
/// Send `req` and parse the reply, refreshing the token before a safe retry after a transport
|
||||
/// error. A mutation that may have reached the server is never resent automatically.
|
||||
fn exchange(&mut self, method: &str, req: &mut Value) -> Result<Value, String> {
|
||||
let reply = match self.sign_and_send(req) {
|
||||
let reply = match self.roundtrip(&req.to_string()) {
|
||||
Ok(r) => r,
|
||||
Err((e, sent)) => {
|
||||
self.token = None;
|
||||
@@ -248,7 +232,8 @@ impl Remote {
|
||||
));
|
||||
}
|
||||
// A restarted server may have rotated its token before dropping the old socket.
|
||||
self.sign_and_send(req).map_err(|(e, _)| {
|
||||
self.set_token(req);
|
||||
self.roundtrip(&req.to_string()).map_err(|(e, _)| {
|
||||
self.token = None;
|
||||
self.conn = None;
|
||||
format!("CADCraft at {}: {e}", self.addr)
|
||||
@@ -271,12 +256,14 @@ impl Backend for Remote {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let mut req = json!({"id": id, "method": method, "params": params});
|
||||
self.set_token(&mut req);
|
||||
let mut v = self.exchange(method, &mut req)?;
|
||||
// The token is read before `--launch` opens the app, which then writes a new one (#2087); the
|
||||
// file can also be left over from an earlier session. Once, so a wrong token can't loop.
|
||||
if unauthorized(&v) && v.get("state").and_then(Value::as_str) == Some("not-run") {
|
||||
self.token = None;
|
||||
self.conn = None;
|
||||
self.set_token(&mut req);
|
||||
v = self.exchange(method, &mut req)?;
|
||||
}
|
||||
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||
@@ -608,24 +595,6 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
/// A listener on another loopback address with the desktop's port (here `::1`) never receives
|
||||
/// the desktop's token.
|
||||
#[test]
|
||||
fn token_is_not_sent_to_another_loopback_listener() {
|
||||
let Ok(listener) = TcpListener::bind("[::1]:0") else { return };
|
||||
let addr = listener.local_addr().unwrap();
|
||||
crate::control_token::issue(addr.port()).unwrap();
|
||||
let server = fake_app(listener, move |req, _| {
|
||||
assert!(req.get("token").is_none(), "token leaked to {addr}");
|
||||
json!({"ok": true, "result": "x"})
|
||||
});
|
||||
let mut remote = Remote::lazy(&addr.to_string(), false);
|
||||
let got = remote.call("ping", json!({}));
|
||||
crate::control_token::revoke(addr.port());
|
||||
assert_eq!(got, Ok(json!("x")));
|
||||
assert_eq!(server.join().unwrap(), 1);
|
||||
}
|
||||
|
||||
/// `--launch` reads the token before it opens the app, so the first request carries the token
|
||||
/// of an earlier session (or none); the app refuses it without running it, and the call is
|
||||
/// resent once with the token the app has just written (#2087).
|
||||
|
||||
@@ -32,9 +32,7 @@ fn private_dir() -> io::Result<PathBuf> {
|
||||
if let Some(h) = std::env::var_os("HOME").filter(|v| !v.is_empty()) {
|
||||
return Ok(PathBuf::from(h).join(".cache"));
|
||||
}
|
||||
// The shared temporary directory would put the credential at a path any local account can
|
||||
// predict and plant a link at: refuse instead.
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "XDG_RUNTIME_DIR or HOME is required for a private control token"))
|
||||
Ok(std::env::temp_dir())
|
||||
}
|
||||
|
||||
/// Require the Windows per-user local app-data directory for session token storage.
|
||||
@@ -77,31 +75,7 @@ fn issue_at(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
|
||||
|
||||
/// Read the token for `port`, trimming trailing whitespace. Used by clients (the MCP bridge).
|
||||
pub fn read(port: u16) -> io::Result<String> {
|
||||
read_at(&token_path(port)?)
|
||||
}
|
||||
|
||||
/// Read a token file the issuer wrote: not through a link, a plain file, and (Unix) readable by
|
||||
/// its owner only, so a file another account planted or left open is not taken as the credential.
|
||||
fn read_at(path: &std::path::Path) -> io::Result<String> {
|
||||
use std::io::Read;
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
|
||||
}
|
||||
let file = options.open(path)?;
|
||||
let meta = file.metadata()?;
|
||||
if !meta.is_file() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "the control token is not a plain file"));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if std::os::unix::fs::PermissionsExt::mode(&meta.permissions()) & 0o077 != 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "the control token file is readable by other users"));
|
||||
}
|
||||
let mut raw = String::new();
|
||||
file.take(1024).read_to_string(&mut raw)?;
|
||||
let raw = std::fs::read_to_string(token_path(port)?)?;
|
||||
let token = raw.trim().to_string();
|
||||
if token.is_empty() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "empty control token"));
|
||||
@@ -207,24 +181,6 @@ mod tests {
|
||||
assert_eq!(windows_private_dir(Some("private-app-data".into())).unwrap(), PathBuf::from("private-app-data"));
|
||||
}
|
||||
|
||||
/// A link, or a file other users can read, is not accepted as the token.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn read_refuses_links_and_shared_files() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-token-read-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let secret = dir.join("secret");
|
||||
issue_at(&secret, b"abc").unwrap();
|
||||
assert_eq!(read_at(&secret).unwrap(), "abc");
|
||||
let link = dir.join("link");
|
||||
std::os::unix::fs::symlink(&secret, &link).unwrap();
|
||||
assert!(read_at(&link).is_err());
|
||||
std::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o644)).unwrap();
|
||||
assert!(read_at(&secret).is_err());
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// Token issuance creates its missing directory hierarchy before writing the credential.
|
||||
#[test]
|
||||
fn issuance_creates_missing_parent() {
|
||||
|
||||
@@ -128,10 +128,7 @@ const MAX_DEDUP_COMPARISONS: u64 = 50_000_000;
|
||||
/// within islands alternate), 1 outer keeps the outermost loops and their first islands, 2
|
||||
/// ignore keeps only the outermost loops. Nesting depth = how many other loops enclose a loop.
|
||||
pub fn fill_loops(h: &Hatch, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
// One point budget for all the loops: many short arcs across loops must not add up unbounded.
|
||||
let mut budget = cadcraft_geom::MAX_TESSELLATED_POINTS;
|
||||
let mut loops: Vec<Vec<Vec2>> =
|
||||
h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate_within(tol, &mut budget)).collect();
|
||||
let mut loops: Vec<Vec<Vec2>> = h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
|
||||
dedup_polygons(&mut loops);
|
||||
let max_depth = match h.style {
|
||||
1 => 1,
|
||||
|
||||
@@ -77,16 +77,10 @@ fn rows(app: &CadApp, ui: &egui::Ui) -> Vec<Row> {
|
||||
let xrefs: Vec<_> = d.blocks.values().filter(|b| b.xref_path.is_some() && !b.name.contains('|')).collect();
|
||||
let mut counts = vec![0usize; xrefs.len()];
|
||||
if !xrefs.is_empty() {
|
||||
// Xref name (ASCII-upper-cased) → its row, the first of names that differ only in case, so
|
||||
// each INSERT is one lookup instead of a scan of the xref list.
|
||||
let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
for (k, b) in xrefs.iter().enumerate() {
|
||||
index.entry(b.name.to_ascii_uppercase()).or_insert(k);
|
||||
}
|
||||
let mut count = |store: &cadcraft_engine::doc::EntityStore| {
|
||||
for e in store.iter() {
|
||||
if let cadcraft_engine::doc::EntityKind::Insert(i) = &e.kind
|
||||
&& let Some(&k) = index.get(&i.block.to_ascii_uppercase())
|
||||
&& let Some(k) = xrefs.iter().position(|b| b.name.eq_ignore_ascii_case(&i.block))
|
||||
&& let Some(c) = counts.get_mut(k)
|
||||
{
|
||||
*c += 1;
|
||||
|
||||
@@ -14,7 +14,7 @@ below, and ≈ 20–40 h for 3D exchange formats that wait for 3D modelling.
|
||||
|
||||
## How this was measured
|
||||
|
||||
- CADCraft: read from `crates/dxf`, `crates/io` (`dxf_read.rs`, `dxf_write.rs`, `pdf.rs`,
|
||||
- CADCraft: read from `craft-dxf` (craft-libs), `crates/io` (`dxf_read.rs`, `dxf_write.rs`, `pdf.rs`,
|
||||
`svg.rs`, `tests.rs`) and `crates/dwg` on 2026-10-10. Nothing was run (the measurement pass
|
||||
did no builds).
|
||||
- AutoCAD: the formats in its Open/Save As/Import/Export/Attach/Plot dialogs, from Autodesk's
|
||||
|
||||
@@ -34,7 +34,6 @@ impl Class {
|
||||
/// prefix.
|
||||
pub const TABLE: &[(&str, Class)] = &[
|
||||
("geom", Class::Layer(0)),
|
||||
("dxf", Class::Standalone),
|
||||
("dwg", Class::Standalone),
|
||||
("color", Class::Layer(0)),
|
||||
("doc", Class::Layer(1)),
|
||||
|
||||
Reference in New Issue
Block a user