From 79c2003ac9c665bfd6a0dbc00c256d42cc3ad1e3 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 00:29:48 -0500 Subject: [PATCH 01/12] Add shared file format detection --- Cargo.lock | 7 +++ Cargo.toml | 1 + crates/file-formats/Cargo.toml | 14 +++++ crates/file-formats/src/lib.rs | 93 ++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 crates/file-formats/Cargo.toml create mode 100644 crates/file-formats/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cf0e0bd47..5e004a7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1454,6 +1454,13 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "craft-file-formats" +version = "0.1.0" +dependencies = [ + "craft-eps", +] + [[package]] name = "craft-flags" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 629bab64c..4af6f586a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ craft-docx = { path = "crates/docx" } craft-dxf = { path = "crates/dxf" } craft-drawingml = { path = "crates/drawingml" } craft-eps = { path = "crates/eps" } +craft-file-formats = { path = "crates/file-formats" } craft-doc = { path = "crates/doc" } craft-docpath = { path = "crates/docpath" } craft-dpx = { path = "crates/dpx" } diff --git a/crates/file-formats/Cargo.toml b/crates/file-formats/Cargo.toml new file mode 100644 index 000000000..0a5be5e76 --- /dev/null +++ b/crates/file-formats/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "craft-file-formats" +description = "Shared bounded file signature and extension resolution for Craft applications" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +craft-eps = { workspace = true } + +[lints] +workspace = true diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs new file mode 100644 index 000000000..b51fc399c --- /dev/null +++ b/crates/file-formats/src/lib.rs @@ -0,0 +1,93 @@ +//! Bounded file kind detection and extension resolution shared by Craft applications. +//! +//! This crate identifies a format; it does not parse, validate, or decode the file. +//! Callers remain responsible for choosing which identified formats their product supports. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#![forbid(unsafe_code)] + +/// A supported file family, independent of the application model that consumes it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Format { + Pdf, + Eps, + PostScript, + Illustrator, + Dxf, + Svg, + Raster, + Studio, + StencilCraft, +} + +/// Resolve a name and file bytes to a format. Strong signatures take precedence over +/// extension hints. Only format-specific bounded probes and extension tables live here. +pub fn detect(name: &str, bytes: &[u8]) -> Option { + if let Some(found) = craft_eps::sniff(bytes) { + return Some(match (found.kind, found.illustrator) { + (_, true) => Format::Illustrator, + (craft_eps::Kind::Pdf, _) => Format::Pdf, + (craft_eps::Kind::Eps, _) => Format::Eps, + (craft_eps::Kind::PostScript, _) => Format::PostScript, + }); + } + if raster_signature(bytes) { + return Some(Format::Raster); + } + if bytes.starts_with(b"silhouette04;") || bytes.starts_with(b"silhouette05;") { + return Some(Format::Studio); + } + if let Some(ext) = extension(name) { + if let Some(format) = from_extension(ext) { + return Some(format); + } + } + let head = bytes.get(..4096).unwrap_or(bytes); + let xml = head.trim_ascii_start(); + if xml.starts_with(b" Option { + match ext.trim_start_matches('.').to_ascii_lowercase().as_str() { + "pdf" => Some(Format::Pdf), + "eps" => Some(Format::Eps), + "ps" => Some(Format::PostScript), + "ai" => Some(Format::Illustrator), + "dxf" => Some(Format::Dxf), + "svg" | "svgz" => Some(Format::Svg), + "png" | "jpg" | "jpeg" | "bmp" | "gif" | "tif" | "tiff" | "webp" | "heic" | "heif" => Some(Format::Raster), + "studio" | "studio3" => Some(Format::Studio), + "stencilcraft" => Some(Format::StencilCraft), + _ => None, + } +} + +fn extension(name: &str) -> Option<&str> { + Some(name.rsplit(['/', '\\']).next()?.rsplit_once('.')?.1) +} + +fn dxf_ascii_prefix(bytes: &[u8]) -> bool { + let text = String::from_utf8_lossy(bytes); + let mut lines = text.lines().map(str::trim); + matches!((lines.next(), lines.next(), lines.next(), lines.next()), (Some("0"), Some("SECTION"), Some("2"), Some(section)) if matches!(section, "HEADER" | "CLASSES" | "TABLES" | "BLOCKS" | "ENTITIES" | "OBJECTS")) +} + +fn raster_signature(bytes: &[u8]) -> bool { + let head = bytes.get(..16).unwrap_or(bytes); + head.starts_with(b"\x89PNG\r\n\x1a\n") + || head.starts_with(b"\xff\xd8\xff") + || head.starts_with(b"BM") + || head.starts_with(b"GIF87a") + || head.starts_with(b"GIF89a") + || head.starts_with(b"II*\0") + || head.starts_with(b"MM\0*") + || head.starts_with(b"II+\0") + || head.starts_with(b"MM\0+") + || (head.starts_with(b"RIFF") && head.get(8..12) == Some(b"WEBP")) +} From 8d3c753b5e7422d4484f5550b6e84bc93f2ab280 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 00:34:11 -0500 Subject: [PATCH 02/12] Recognize G-code filename extensions --- crates/file-formats/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index b51fc399c..c5a2164de 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -15,6 +15,7 @@ pub enum Format { Dxf, Svg, Raster, + Gcode, Studio, StencilCraft, } @@ -64,6 +65,7 @@ pub fn from_extension(ext: &str) -> Option { "png" | "jpg" | "jpeg" | "bmp" | "gif" | "tif" | "tiff" | "webp" | "heic" | "heif" => Some(Format::Raster), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), + "gcode" | "nc" => Some(Format::Gcode), _ => None, } } From c1a1b976d0decf496d132bf71eecaec0de198675 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 00:34:48 -0500 Subject: [PATCH 03/12] Resolve raster subformats centrally --- crates/file-formats/src/lib.rs | 51 ++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index c5a2164de..29bf92a3f 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -14,7 +14,13 @@ pub enum Format { Illustrator, Dxf, Svg, - Raster, + Png, + Jpeg, + Bmp, + Gif, + Tiff, + WebP, + Heif, Gcode, Studio, StencilCraft, @@ -31,8 +37,8 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { (craft_eps::Kind::PostScript, _) => Format::PostScript, }); } - if raster_signature(bytes) { - return Some(Format::Raster); + if let Some(raster) = raster_signature(bytes) { + return Some(raster); } if bytes.starts_with(b"silhouette04;") || bytes.starts_with(b"silhouette05;") { return Some(Format::Studio); @@ -62,7 +68,13 @@ pub fn from_extension(ext: &str) -> Option { "ai" => Some(Format::Illustrator), "dxf" => Some(Format::Dxf), "svg" | "svgz" => Some(Format::Svg), - "png" | "jpg" | "jpeg" | "bmp" | "gif" | "tif" | "tiff" | "webp" | "heic" | "heif" => Some(Format::Raster), + "png" => Some(Format::Png), + "jpg" | "jpeg" => Some(Format::Jpeg), + "bmp" => Some(Format::Bmp), + "gif" => Some(Format::Gif), + "tif" | "tiff" => Some(Format::Tiff), + "webp" => Some(Format::WebP), + "heic" | "heif" => Some(Format::Heif), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode), @@ -80,16 +92,25 @@ fn dxf_ascii_prefix(bytes: &[u8]) -> bool { matches!((lines.next(), lines.next(), lines.next(), lines.next()), (Some("0"), Some("SECTION"), Some("2"), Some(section)) if matches!(section, "HEADER" | "CLASSES" | "TABLES" | "BLOCKS" | "ENTITIES" | "OBJECTS")) } -fn raster_signature(bytes: &[u8]) -> bool { +fn raster_signature(bytes: &[u8]) -> Option { let head = bytes.get(..16).unwrap_or(bytes); - head.starts_with(b"\x89PNG\r\n\x1a\n") - || head.starts_with(b"\xff\xd8\xff") - || head.starts_with(b"BM") - || head.starts_with(b"GIF87a") - || head.starts_with(b"GIF89a") - || head.starts_with(b"II*\0") - || head.starts_with(b"MM\0*") - || head.starts_with(b"II+\0") - || head.starts_with(b"MM\0+") - || (head.starts_with(b"RIFF") && head.get(8..12) == Some(b"WEBP")) + if head.starts_with(b"\x89PNG\r\n\x1a\n") { + return Some(Format::Png); + } + if head.starts_with(b"\xff\xd8\xff") { + return Some(Format::Jpeg); + } + if head.starts_with(b"BM") { + return Some(Format::Bmp); + } + if head.starts_with(b"GIF87a") || head.starts_with(b"GIF89a") { + return Some(Format::Gif); + } + if head.starts_with(b"II*\0") || head.starts_with(b"MM\0*") || head.starts_with(b"II+\0") || head.starts_with(b"MM\0+") { + return Some(Format::Tiff); + } + if head.starts_with(b"RIFF") && head.get(8..12) == Some(b"WEBP") { + return Some(Format::WebP); + } + None } From d8cc88a7720755e0054038ebc0e152b2e0dcfa00 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 00:39:31 -0500 Subject: [PATCH 04/12] Add image document formats to shared registry --- crates/file-formats/src/lib.rs | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 29bf92a3f..9d3a3c438 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -21,6 +21,14 @@ pub enum Format { Tiff, WebP, Heif, + Psd, + Affinity, + Ora, + Kra, + Xcf, + Pdn, + Raw, + Pcraft, Gcode, Studio, StencilCraft, @@ -37,6 +45,18 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { (craft_eps::Kind::PostScript, _) => Format::PostScript, }); } + if bytes.starts_with(b"8BPS") { + return Some(Format::Psd); + } + if bytes.starts_with(b"PDN3") { + return Some(Format::Pdn); + } + if bytes.starts_with(b"gimp xcf ") || gzipped_xcf(name, bytes) { + return Some(Format::Xcf); + } + if bytes.starts_with(b"\0\xffKA") { + return Some(Format::Affinity); + } if let Some(raster) = raster_signature(bytes) { return Some(raster); } @@ -75,6 +95,16 @@ pub fn from_extension(ext: &str) -> Option { "tif" | "tiff" => Some(Format::Tiff), "webp" => Some(Format::WebP), "heic" | "heif" => Some(Format::Heif), + "psd" | "psb" => Some(Format::Psd), + "afdesign" | "afphoto" | "afpub" => Some(Format::Affinity), + "ora" => Some(Format::Ora), + "kra" => Some(Format::Kra), + "xcf" => Some(Format::Xcf), + "pdn" => Some(Format::Pdn), + "dng" | "3fr" | "ari" | "arw" | "bay" | "cap" | "cr2" | "cr3" | "dcs" | "dcr" | "drf" | "eip" | "erf" | "fff" | "gpr" | "iiq" | "k25" + | "kdc" | "mdc" | "mef" | "mos" | "mrw" | "nef" | "nrw" | "obm" | "orf" | "pef" | "ptx" | "pxn" | "r3d" | "raf" | "raw" | "rwl" | "rw2" + | "rwz" | "sr2" | "srf" | "sti" | "x3f" => Some(Format::Raw), + "pcraft" => Some(Format::Pcraft), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode), @@ -86,6 +116,11 @@ fn extension(name: &str) -> Option<&str> { Some(name.rsplit(['/', '\\']).next()?.rsplit_once('.')?.1) } +fn gzipped_xcf(name: &str, bytes: &[u8]) -> bool { + let lower = name.rsplit(['/', '\\']).next().unwrap_or(name).to_ascii_lowercase(); + bytes.starts_with(&[0x1f, 0x8b]) && (lower.ends_with(".xcf.gz") || lower.ends_with(".xcfgz")) +} + fn dxf_ascii_prefix(bytes: &[u8]) -> bool { let text = String::from_utf8_lossy(bytes); let mut lines = text.lines().map(str::trim); From fc9ecb40e72bb3d539ae16d158b84ebed4c65024 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:22:58 -0500 Subject: [PATCH 05/12] Extend shared file-format registry --- Cargo.lock | 1 + crates/file-formats/Cargo.toml | 1 + crates/file-formats/src/lib.rs | 39 +++++++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e004a7ce..87ef135e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ name = "craft-file-formats" version = "0.1.0" dependencies = [ "craft-eps", + "craft-svg", ] [[package]] diff --git a/crates/file-formats/Cargo.toml b/crates/file-formats/Cargo.toml index 0a5be5e76..00d3b388a 100644 --- a/crates/file-formats/Cargo.toml +++ b/crates/file-formats/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] craft-eps = { workspace = true } +craft-svg = { workspace = true } [lints] workspace = true diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 9d3a3c438..9df7fe16a 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -14,6 +14,8 @@ pub enum Format { Illustrator, Dxf, Svg, + /// Gzip-compressed SVG. Kept distinct so writers do not place plain SVG under `.svgz`. + Svgz, Png, Jpeg, Bmp, @@ -29,6 +31,13 @@ pub enum Format { Pdn, Raw, Pcraft, + Idml, + DesignCraft, + Docx, + Rtf, + Xlsx, + TaggedText, + Pptx, Gcode, Studio, StencilCraft, @@ -63,16 +72,29 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { if bytes.starts_with(b"silhouette04;") || bytes.starts_with(b"silhouette05;") { return Some(Format::Studio); } + let tagged = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes).trim_ascii_start(); + if tagged.starts_with(b" Option { "ps" => Some(Format::PostScript), "ai" => Some(Format::Illustrator), "dxf" => Some(Format::Dxf), - "svg" | "svgz" => Some(Format::Svg), + "svg" => Some(Format::Svg), + "svgz" => Some(Format::Svgz), "png" => Some(Format::Png), "jpg" | "jpeg" => Some(Format::Jpeg), "bmp" => Some(Format::Bmp), @@ -105,6 +128,12 @@ pub fn from_extension(ext: &str) -> Option { | "kdc" | "mdc" | "mef" | "mos" | "mrw" | "nef" | "nrw" | "obm" | "orf" | "pef" | "ptx" | "pxn" | "r3d" | "raf" | "raw" | "rwl" | "rw2" | "rwz" | "sr2" | "srf" | "sti" | "x3f" => Some(Format::Raw), "pcraft" => Some(Format::Pcraft), + "idml" => Some(Format::Idml), + "designcraft" | "dcdoc" => Some(Format::DesignCraft), + "docx" => Some(Format::Docx), + "rtf" => Some(Format::Rtf), + "xlsx" => Some(Format::Xlsx), + "pptx" => Some(Format::Pptx), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode), From 32529b94e8bfcc2febc08ef9d00dc97187e8e7e2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:24:40 -0500 Subject: [PATCH 06/12] Resolve plain text format extensions --- crates/file-formats/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 9df7fe16a..6029757de 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -38,6 +38,7 @@ pub enum Format { Xlsx, TaggedText, Pptx, + PlainText, Gcode, Studio, StencilCraft, @@ -134,6 +135,7 @@ pub fn from_extension(ext: &str) -> Option { "rtf" => Some(Format::Rtf), "xlsx" => Some(Format::Xlsx), "pptx" => Some(Format::Pptx), + "txt" | "text" | "md" => Some(Format::PlainText), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode), From 1f9c271f7d0924a50b84032dcde90b259ae3ccf9 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:26:09 -0500 Subject: [PATCH 07/12] Recognize legacy Word extension in shared registry --- crates/file-formats/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 6029757de..3c6001673 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -34,6 +34,8 @@ pub enum Format { Idml, DesignCraft, Docx, + /// Legacy OLE Compound File Word document; identified by its extension only. + LegacyDoc, Rtf, Xlsx, TaggedText, @@ -132,6 +134,7 @@ pub fn from_extension(ext: &str) -> Option { "idml" => Some(Format::Idml), "designcraft" | "dcdoc" => Some(Format::DesignCraft), "docx" => Some(Format::Docx), + "doc" => Some(Format::LegacyDoc), "rtf" => Some(Format::Rtf), "xlsx" => Some(Format::Xlsx), "pptx" => Some(Format::Pptx), From 6ba1ca33ca21c33de55ef155141db08ca6048e8d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:27:11 -0500 Subject: [PATCH 08/12] Recognize InDesign files in shared registry --- crates/file-formats/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 3c6001673..1e45ad447 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -33,6 +33,7 @@ pub enum Format { Pcraft, Idml, DesignCraft, + Indd, Docx, /// Legacy OLE Compound File Word document; identified by its extension only. LegacyDoc, @@ -49,6 +50,9 @@ pub enum Format { /// Resolve a name and file bytes to a format. Strong signatures take precedence over /// extension hints. Only format-specific bounded probes and extension tables live here. pub fn detect(name: &str, bytes: &[u8]) -> Option { + if bytes.starts_with(&INDD_SIGNATURE) { + return Some(Format::Indd); + } if let Some(found) = craft_eps::sniff(bytes) { return Some(match (found.kind, found.illustrator) { (_, true) => Format::Illustrator, @@ -133,6 +137,7 @@ pub fn from_extension(ext: &str) -> Option { "pcraft" => Some(Format::Pcraft), "idml" => Some(Format::Idml), "designcraft" | "dcdoc" => Some(Format::DesignCraft), + "indd" | "indt" => Some(Format::Indd), "docx" => Some(Format::Docx), "doc" => Some(Format::LegacyDoc), "rtf" => Some(Format::Rtf), @@ -146,6 +151,8 @@ pub fn from_extension(ext: &str) -> Option { } } +const INDD_SIGNATURE: [u8; 16] = [0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D]; + fn extension(name: &str) -> Option<&str> { Some(name.rsplit(['/', '\\']).next()?.rsplit_once('.')?.1) } From b4ada28f19067d4693e902e0e5f32eb292241e3a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:37:48 -0500 Subject: [PATCH 09/12] Fix format registry clippy and add ASE support --- crates/file-formats/src/lib.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 1e45ad447..c6ac77540 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -23,6 +23,7 @@ pub enum Format { Tiff, WebP, Heif, + Ase, Psd, Affinity, Ora, @@ -64,6 +65,9 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { if bytes.starts_with(b"8BPS") { return Some(Format::Psd); } + if bytes.starts_with(b"ASEF") { + return Some(Format::Ase); + } if bytes.starts_with(b"PDN3") { return Some(Format::Pdn); } @@ -86,20 +90,19 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { if bytes.trim_ascii_start().starts_with(b"{\\rtf") { return Some(Format::Rtf); } - if craft_svg::sniff::is_svgz(bytes) { - if let Ok(text) = craft_svg::svgz::text(bytes, 64 * 1024 * 1024) - && craft_svg::sniff::is_svg(text.as_bytes()) - { - return Some(Format::Svgz); - } + if craft_svg::sniff::is_svgz(bytes) + && let Ok(text) = craft_svg::svgz::text(bytes, 64 * 1024 * 1024) + && craft_svg::sniff::is_svg(text.as_bytes()) + { + return Some(Format::Svgz); } if craft_svg::sniff::is_svg(bytes) { return Some(Format::Svg); } - if let Some(ext) = extension(name) { - if let Some(format) = from_extension(ext) { - return Some(format); - } + if let Some(ext) = extension(name) + && let Some(format) = from_extension(ext) + { + return Some(format); } let head = bytes.get(..4096).unwrap_or(bytes); if dxf_ascii_prefix(head) || head.starts_with(b"AutoCAD Binary DXF\r\n\x1a\0") { @@ -125,6 +128,7 @@ pub fn from_extension(ext: &str) -> Option { "tif" | "tiff" => Some(Format::Tiff), "webp" => Some(Format::WebP), "heic" | "heif" => Some(Format::Heif), + "ase" => Some(Format::Ase), "psd" | "psb" => Some(Format::Psd), "afdesign" | "afphoto" | "afpub" => Some(Format::Affinity), "ora" => Some(Format::Ora), From fccc0082711b8ae4e19824dd20339a8473d47309 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:44:49 -0500 Subject: [PATCH 10/12] Detect DesignCraft and IDML document content --- Cargo.lock | 4 +++ crates/file-formats/Cargo.toml | 4 +++ crates/file-formats/src/lib.rs | 66 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 87ef135e7..8f13c4412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,7 +1459,11 @@ name = "craft-file-formats" version = "0.1.0" dependencies = [ "craft-eps", + "craft-idml", "craft-svg", + "craft-zip", + "serde", + "serde_json", ] [[package]] diff --git a/crates/file-formats/Cargo.toml b/crates/file-formats/Cargo.toml index 00d3b388a..a1a605918 100644 --- a/crates/file-formats/Cargo.toml +++ b/crates/file-formats/Cargo.toml @@ -9,7 +9,11 @@ rust-version.workspace = true [dependencies] craft-eps = { workspace = true } +craft-idml = { workspace = true } craft-svg = { workspace = true } +craft-zip = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } [lints] workspace = true diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index c6ac77540..1bdfc3b5c 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -34,6 +34,8 @@ pub enum Format { Pcraft, Idml, DesignCraft, + /// Legacy DesignCraft documents serialized as one JSON object. + DesignCraftJson, Indd, Docx, /// Legacy OLE Compound File Word document; identified by its extension only. @@ -51,6 +53,15 @@ pub enum Format { /// Resolve a name and file bytes to a format. Strong signatures take precedence over /// extension hints. Only format-specific bounded probes and extension tables live here. pub fn detect(name: &str, bytes: &[u8]) -> Option { + if craft_idml::is_idml(bytes) { + return Some(Format::Idml); + } + if craft_zip::stored_mimetype(bytes) == Some(b"application/vnd.designcraft+zip") || designcraft_zip_has_document(bytes) { + return Some(Format::DesignCraft); + } + if is_designcraft_json(bytes) { + return Some(Format::DesignCraftJson); + } if bytes.starts_with(&INDD_SIGNATURE) { return Some(Format::Indd); } @@ -111,6 +122,61 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option { None } +/// DesignCraft's older ZIPs predate their leading `mimetype`; they are identified by the +/// `document.json` member. The directory is bounded before it is indexed and no entry is inflated. +fn designcraft_zip_has_document(bytes: &[u8]) -> bool { + if !bytes.starts_with(b"PK") { + return false; + } + craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 100_000, max_total_bytes: 0 }) + .is_ok_and(|archive| archive.contains("document.json")) +} + +/// Read only a bounded prefix, stopping as soon as the model's required top-level keys have +/// appeared. This distinguishes legacy DesignCraft JSON from arbitrary JSON without decoding the +/// document or walking its arrays and assets. +fn is_designcraft_json(bytes: &[u8]) -> bool { + const LIMIT: usize = 1 << 20; + const REQUIRED: u16 = (1 << 10) - 1; + const KEYS: [&str; 10] = ["title", "settings", "spreads", "parents", "layers", "stories", "styles", "swatches", "sections", "next_id"]; + + struct Probe; + impl<'de> serde::de::Visitor<'de> for Probe { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a DesignCraft document object") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut found = 0u16; + while let Some(key) = map.next_key::()? { + if let Some(index) = KEYS.iter().position(|candidate| *candidate == key) { + found |= 1 << index; + } + map.next_value::()?; + if found == REQUIRED { + return Ok(true); + } + } + Ok(false) + } + } + + use serde::de::DeserializeSeed; + struct Seed; + impl<'de> DeserializeSeed<'de> for Seed { + type Value = bool; + fn deserialize>(self, deserializer: D) -> Result { + deserializer.deserialize_map(Probe) + } + } + + let prefix = bytes.get(..bytes.len().min(LIMIT)).unwrap_or(bytes); + let mut deserializer = serde_json::Deserializer::from_slice(prefix); + Seed.deserialize(&mut deserializer).unwrap_or(false) +} + /// Resolve a file extension to a family, case-insensitively. pub fn from_extension(ext: &str) -> Option { match ext.trim_start_matches('.').to_ascii_lowercase().as_str() { From 71337f6772b57a61e0f171c941f695822eea19a5 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:51:08 -0500 Subject: [PATCH 11/12] Add shared CSV and TSV file formats --- crates/file-formats/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 1bdfc3b5c..a570f0af4 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -45,6 +45,8 @@ pub enum Format { TaggedText, Pptx, PlainText, + Csv, + Tsv, Gcode, Studio, StencilCraft, @@ -214,6 +216,8 @@ pub fn from_extension(ext: &str) -> Option { "xlsx" => Some(Format::Xlsx), "pptx" => Some(Format::Pptx), "txt" | "text" | "md" => Some(Format::PlainText), + "csv" => Some(Format::Csv), + "tsv" | "tab" => Some(Format::Tsv), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode), From 8abfa9d0f14bafbfc3449fc552ffeafb9ca6a551 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:53:37 -0500 Subject: [PATCH 12/12] Classify legacy Excel extensions in shared registry --- crates/file-formats/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index a570f0af4..7859bf539 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -47,6 +47,8 @@ pub enum Format { PlainText, Csv, Tsv, + /// Legacy binary Excel workbooks, identified by extension only. + LegacyExcel, Gcode, Studio, StencilCraft, @@ -218,6 +220,7 @@ pub fn from_extension(ext: &str) -> Option { "txt" | "text" | "md" => Some(Format::PlainText), "csv" => Some(Format::Csv), "tsv" | "tab" => Some(Format::Tsv), + "xls" | "xlsm" => Some(Format::LegacyExcel), "studio" | "studio3" => Some(Format::Studio), "stencilcraft" => Some(Format::StencilCraft), "gcode" | "nc" => Some(Format::Gcode),