mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-15 10:23:43 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305f452283 | ||
|
|
78bcdce794 | ||
|
|
fd11b47915 |
Generated
+6
-19
@@ -714,6 +714,7 @@ version = "0.21.0"
|
||||
dependencies = [
|
||||
"cadcraft-geom",
|
||||
"craft-dxf",
|
||||
"craft-shx",
|
||||
"encoding_rs",
|
||||
"harfrust 0.14.0",
|
||||
"icu_segmenter",
|
||||
@@ -758,8 +759,6 @@ dependencies = [
|
||||
"cadcraft-io",
|
||||
"cadcraft-render",
|
||||
"clap",
|
||||
"craft-jsonrpc",
|
||||
"craft-mcp",
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -1185,23 +1184,6 @@ dependencies = [
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-jsonrpc"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2c9903ca1d892b5446acf3598d8b142f3e64842f#2c9903ca1d892b5446acf3598d8b142f3e64842f"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-mcp"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2c9903ca1d892b5446acf3598d8b142f3e64842f#2c9903ca1d892b5446acf3598d8b142f3e64842f"
|
||||
dependencies = [
|
||||
"craft-jsonrpc",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
@@ -1212,6 +1194,11 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-shx"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=3c5007339754def577b3c5274f523d96384a35c4#3c5007339754def577b3c5274f523d96384a35c4"
|
||||
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-2
@@ -16,6 +16,7 @@ cadcraft-geom = { path = "crates/geom" }
|
||||
# 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" }
|
||||
craft-dwg = { git = "https://github.com/storytold/craft-libs", rev = "7bb6c90076a0177d01cc4c482e7fd8dc9f31a8bf" }
|
||||
craft-shx = { git = "https://github.com/storytold/craft-libs", rev = "3c5007339754def577b3c5274f523d96384a35c4" }
|
||||
cadcraft-color = { path = "crates/color" }
|
||||
cadcraft-doc = { path = "crates/doc" }
|
||||
cadcraft-constraints = { path = "crates/constraints" }
|
||||
@@ -25,8 +26,6 @@ cadcraft-io = { path = "crates/io" }
|
||||
cadcraft-engine = { path = "crates/engine" }
|
||||
cadcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
cadcraft-mcp = { path = "crates/mcp" }
|
||||
craft-mcp = { git = "https://github.com/storytold/craft-libs", rev = "2c9903ca1d892b5446acf3598d8b142f3e64842f" }
|
||||
craft-jsonrpc = { git = "https://github.com/storytold/craft-libs", rev = "2c9903ca1d892b5446acf3598d8b142f3e64842f" }
|
||||
# Which paths stored in a drawing (xrefs, data links) may be opened.
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
# PDF writing (krilla) for plots, and reading them back in tests. The root patches below pin
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
[dependencies]
|
||||
cadcraft-geom = { workspace = true }
|
||||
craft-dxf = { workspace = true }
|
||||
craft-shx = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
harfrust = { workspace = true }
|
||||
unicode-bidi = { workspace = true }
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
//! Shape source (`.shp`) to a compiled SHX file, as COMPILE does.
|
||||
//!
|
||||
//! A source is a list of definitions: a header line `*number,defbytes,NAME` followed by its
|
||||
//! specification bytes (on as many lines as needed, ending with 0). Numbers are decimal, or
|
||||
//! hexadecimal after a leading `0` (`012`, `08cfb`); parentheses only group; `;` starts a
|
||||
//! comment. A big font starts with `*BIGFONT nchars,nranges,first,last,…` and a Unicode font
|
||||
//! with `*UNIFONT,6,name` followed by `above,below,modes,encoding,type,0`. Shape 0 is a text
|
||||
//! font's record (`above,below,modes,0`).
|
||||
//!
|
||||
//! Operands are encoded by what they are: a subshape number takes two bytes in a Unicode font
|
||||
//! (and after `7,0` in a big font), displacements and bulges are two's complement, and an arc's
|
||||
//! direction byte (`-043`) keeps its sign in the high bit. `defbytes` is not checked: the
|
||||
//! program's own length is written.
|
||||
|
||||
use super::ShxError;
|
||||
use super::parse::{BIGFONT, SHAPES_10, UNIFONT};
|
||||
|
||||
/// Largest source compiled.
|
||||
const MAX_SOURCE: usize = 8 << 20;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Kind {
|
||||
Shapes,
|
||||
Big,
|
||||
Uni,
|
||||
}
|
||||
|
||||
struct Shape {
|
||||
number: u16,
|
||||
name: String,
|
||||
program: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A number token: value (sign included).
|
||||
fn number(tok: &str, line: usize) -> Result<i64, ShxError> {
|
||||
let t: String = tok.chars().filter(|c| !matches!(c, '(' | ')') && !c.is_whitespace()).collect();
|
||||
let (neg, digits) = match t.strip_prefix('-') {
|
||||
Some(d) => (true, d),
|
||||
None => (false, t.strip_prefix('+').unwrap_or(&t)),
|
||||
};
|
||||
let err = || ShxError::Source(line, format!("not a number: {tok:?}"));
|
||||
let v = if digits.len() > 1 && digits.starts_with('0') {
|
||||
i64::from_str_radix(digits.get(1..).unwrap_or_default(), 16).map_err(|_| err())?
|
||||
} else {
|
||||
digits.parse::<i64>().map_err(|_| err())?
|
||||
};
|
||||
if v > 0xFFFF {
|
||||
return Err(err());
|
||||
}
|
||||
Ok(if neg { -v } else { v })
|
||||
}
|
||||
|
||||
/// The comma-separated numbers of a line (comment removed, blank fields skipped).
|
||||
fn numbers(text: &str, line: usize) -> Result<Vec<i64>, ShxError> {
|
||||
text.split(',').map(str::trim).filter(|t| !t.is_empty() && *t != "(" && *t != ")").map(|t| number(t, line)).collect()
|
||||
}
|
||||
|
||||
/// The spec-byte encoder: walks the tokens as the reader walks the bytes.
|
||||
struct Enc<'a> {
|
||||
toks: &'a [i64],
|
||||
i: usize,
|
||||
out: Vec<u8>,
|
||||
kind: Kind,
|
||||
line: usize,
|
||||
}
|
||||
|
||||
impl Enc<'_> {
|
||||
fn next(&mut self) -> Result<i64, ShxError> {
|
||||
let v = self.toks.get(self.i).copied().ok_or_else(|| ShxError::Source(self.line, "the shape ends inside a command".into()))?;
|
||||
self.i += 1;
|
||||
Ok(v)
|
||||
}
|
||||
fn byte(&mut self) -> Result<(), ShxError> {
|
||||
let v = self.next()?;
|
||||
let b = u8::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not a byte")))?;
|
||||
self.out.push(b);
|
||||
Ok(())
|
||||
}
|
||||
/// Two's complement −128..127.
|
||||
fn signed(&mut self) -> Result<i64, ShxError> {
|
||||
let v = self.next()?;
|
||||
let b = i8::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not −128..127")))?;
|
||||
self.out.extend(b.to_le_bytes());
|
||||
Ok(v)
|
||||
}
|
||||
/// Sign in the high bit (an arc's direction byte).
|
||||
fn sign_magnitude(&mut self) -> Result<(), ShxError> {
|
||||
let v = self.next()?;
|
||||
let m = u8::try_from(v.unsigned_abs())
|
||||
.ok()
|
||||
.filter(|m| *m < 0x80)
|
||||
.ok_or_else(|| ShxError::Source(self.line, format!("{v} is not an octant byte")))?;
|
||||
self.out.push(if v < 0 { m | 0x80 } else { m });
|
||||
Ok(())
|
||||
}
|
||||
fn word(&mut self) -> Result<(), ShxError> {
|
||||
let v = self.next()?;
|
||||
let w = u16::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not a shape number")))?;
|
||||
self.out.extend(w.to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn program(mut self) -> Result<Vec<u8>, ShxError> {
|
||||
while self.i < self.toks.len() {
|
||||
let code = self.next()?;
|
||||
let c = u8::try_from(code).map_err(|_| ShxError::Source(self.line, format!("{code} is not a code")))?;
|
||||
self.out.push(c);
|
||||
match c {
|
||||
0 => break,
|
||||
3 | 4 => self.byte()?,
|
||||
7 => match self.kind {
|
||||
Kind::Uni => self.word()?,
|
||||
Kind::Big => {
|
||||
let first = self.toks.get(self.i).copied().unwrap_or(0);
|
||||
if first == 0 {
|
||||
self.byte()?;
|
||||
self.word()?;
|
||||
for _ in 0..4 {
|
||||
self.byte()?;
|
||||
}
|
||||
} else {
|
||||
self.byte()?;
|
||||
}
|
||||
}
|
||||
Kind::Shapes => self.byte()?,
|
||||
},
|
||||
8 => {
|
||||
self.signed()?;
|
||||
self.signed()?;
|
||||
}
|
||||
9 => loop {
|
||||
let (x, y) = (self.signed()?, self.signed()?);
|
||||
if x == 0 && y == 0 {
|
||||
break;
|
||||
}
|
||||
},
|
||||
10 => {
|
||||
self.byte()?;
|
||||
self.sign_magnitude()?;
|
||||
}
|
||||
11 => {
|
||||
for _ in 0..4 {
|
||||
self.byte()?;
|
||||
}
|
||||
self.sign_magnitude()?;
|
||||
}
|
||||
12 => {
|
||||
for _ in 0..3 {
|
||||
self.signed()?;
|
||||
}
|
||||
}
|
||||
13 => loop {
|
||||
let (x, y) = (self.signed()?, self.signed()?);
|
||||
if x == 0 && y == 0 {
|
||||
break;
|
||||
}
|
||||
self.signed()?;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if self.out.last() != Some(&0) {
|
||||
self.out.push(0);
|
||||
}
|
||||
Ok(self.out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile shape source into an SHX file (a shape file or text font, a big font, or a Unicode
|
||||
/// font, as its first definition says).
|
||||
pub fn compile(source: &str) -> Result<Vec<u8>, ShxError> {
|
||||
if source.len() > MAX_SOURCE {
|
||||
return Err(ShxError::TooLarge);
|
||||
}
|
||||
let mut kind = Kind::Shapes;
|
||||
let mut ranges: Vec<(u16, u16)> = Vec::new();
|
||||
let mut uni_info: Option<(String, Vec<u8>)> = None;
|
||||
// Definitions: header (number, name, line) and the numbers that follow it.
|
||||
let mut defs: Vec<(i64, String, usize, Vec<i64>)> = Vec::new();
|
||||
let mut uni_header_pending = false;
|
||||
for (no, raw) in source.lines().enumerate() {
|
||||
let line = no + 1;
|
||||
let text = raw.split(';').next().unwrap_or_default().trim();
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(head) = text.strip_prefix('*') {
|
||||
let upper = head.trim_start().to_ascii_uppercase();
|
||||
if let Some(rest) = upper.strip_prefix("BIGFONT") {
|
||||
kind = Kind::Big;
|
||||
let v = numbers(rest, line)?;
|
||||
let n = usize::try_from(v.get(1).copied().unwrap_or(0)).unwrap_or(0);
|
||||
for k in 0..n {
|
||||
let lo = v.get(2 + 2 * k).copied().unwrap_or(0);
|
||||
let hi = v.get(3 + 2 * k).copied().unwrap_or(lo);
|
||||
let to = |x: i64| u16::try_from(x).map_err(|_| ShxError::Source(line, "bad escape range".into()));
|
||||
ranges.push((to(lo)?, to(hi)?));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if upper.starts_with("UNIFONT") {
|
||||
kind = Kind::Uni;
|
||||
let name = head.splitn(3, ',').nth(2).unwrap_or_default().trim().to_string();
|
||||
uni_info = Some((name, Vec::new()));
|
||||
uni_header_pending = true;
|
||||
continue;
|
||||
}
|
||||
uni_header_pending = false;
|
||||
let mut parts = head.splitn(3, ',');
|
||||
let num = number(parts.next().unwrap_or_default().trim(), line)?;
|
||||
let _defbytes = parts.next();
|
||||
let name = parts.next().unwrap_or_default().trim().to_string();
|
||||
defs.push((num, name, line, Vec::new()));
|
||||
continue;
|
||||
}
|
||||
let v = numbers(text, line)?;
|
||||
if uni_header_pending {
|
||||
if let Some((_, info)) = uni_info.as_mut() {
|
||||
for x in v {
|
||||
info.push(u8::try_from(x).map_err(|_| ShxError::Source(line, "bad font record".into()))?);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match defs.last_mut() {
|
||||
Some(d) => d.3.extend(v),
|
||||
None => return Err(ShxError::Source(line, "specification bytes before any *number,defbytes,NAME line".into())),
|
||||
}
|
||||
}
|
||||
let mut shapes: Vec<Shape> = Vec::new();
|
||||
for (num, name, line, toks) in defs {
|
||||
let number = u16::try_from(num).map_err(|_| ShxError::Source(line, format!("shape number {num} out of range")))?;
|
||||
let program = if number == 0 {
|
||||
// The font record: plain bytes.
|
||||
toks.iter().map(|x| u8::try_from(*x).map_err(|_| ShxError::Source(line, "bad font record".into()))).collect::<Result<Vec<u8>, _>>()?
|
||||
} else {
|
||||
Enc { toks: &toks, i: 0, out: Vec::new(), kind, line }.program()?
|
||||
};
|
||||
if shapes.iter().any(|s| s.number == number) {
|
||||
return Err(ShxError::Source(line, format!("shape {number} is defined twice")));
|
||||
}
|
||||
shapes.push(Shape { number, name, program });
|
||||
}
|
||||
if shapes.is_empty() {
|
||||
return Err(ShxError::Empty);
|
||||
}
|
||||
shapes.sort_by_key(|s| s.number);
|
||||
let record = |s: &Shape| -> Result<Vec<u8>, ShxError> {
|
||||
let mut r: Vec<u8> = s.name.bytes().filter(|b| *b != 0).collect();
|
||||
r.push(0);
|
||||
r.extend(&s.program);
|
||||
if r.len() > usize::from(u16::MAX) { Err(ShxError::TooLarge) } else { Ok(r) }
|
||||
};
|
||||
let count = u16::try_from(shapes.len()).map_err(|_| ShxError::TooLarge)?;
|
||||
let len16 = |r: &[u8]| u16::try_from(r.len()).unwrap_or(u16::MAX);
|
||||
let mut out = Vec::new();
|
||||
match kind {
|
||||
Kind::Shapes => {
|
||||
out.extend(SHAPES_10);
|
||||
out.extend(shapes.first().map_or(0, |s| s.number).to_le_bytes());
|
||||
out.extend(shapes.last().map_or(0, |s| s.number).to_le_bytes());
|
||||
out.extend(count.to_le_bytes());
|
||||
let recs: Vec<Vec<u8>> = shapes.iter().map(record).collect::<Result<_, _>>()?;
|
||||
for (s, r) in shapes.iter().zip(&recs) {
|
||||
out.extend(s.number.to_le_bytes());
|
||||
out.extend(len16(r).to_le_bytes());
|
||||
}
|
||||
for r in &recs {
|
||||
out.extend(r);
|
||||
}
|
||||
out.extend(b"EOF");
|
||||
}
|
||||
Kind::Big => {
|
||||
out.extend(BIGFONT);
|
||||
let nranges = u16::try_from(ranges.len()).map_err(|_| ShxError::TooLarge)?;
|
||||
out.extend(count.to_le_bytes());
|
||||
out.extend(count.to_le_bytes());
|
||||
out.extend(nranges.to_le_bytes());
|
||||
for (lo, hi) in &ranges {
|
||||
out.extend(lo.to_le_bytes());
|
||||
out.extend(hi.to_le_bytes());
|
||||
}
|
||||
let recs: Vec<Vec<u8>> = shapes.iter().map(record).collect::<Result<_, _>>()?;
|
||||
let mut off = out.len() + 8 * recs.len();
|
||||
for (s, r) in shapes.iter().zip(&recs) {
|
||||
out.extend(s.number.to_le_bytes());
|
||||
out.extend(len16(r).to_le_bytes());
|
||||
out.extend(u32::try_from(off).map_err(|_| ShxError::TooLarge)?.to_le_bytes());
|
||||
off += r.len();
|
||||
}
|
||||
for r in &recs {
|
||||
out.extend(r);
|
||||
}
|
||||
}
|
||||
Kind::Uni => {
|
||||
out.extend(UNIFONT);
|
||||
let (name, info) = uni_info.unwrap_or_default();
|
||||
out.extend((u32::from(count) + 1).to_le_bytes());
|
||||
let mut rec: Vec<u8> = name.bytes().filter(|b| *b != 0).collect();
|
||||
rec.push(0);
|
||||
rec.extend(&info);
|
||||
out.extend(len16(&rec).to_le_bytes());
|
||||
out.extend(&rec);
|
||||
for s in &shapes {
|
||||
let r = record(s)?;
|
||||
out.extend(s.number.to_le_bytes());
|
||||
out.extend(len16(&r).to_le_bytes());
|
||||
out.extend(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -26,7 +26,6 @@
|
||||
//! above, below, modes, encoding, embedding, 0) and then each shape as (number, length,
|
||||
//! record).
|
||||
|
||||
mod compile;
|
||||
mod parse;
|
||||
mod program;
|
||||
mod table;
|
||||
@@ -39,7 +38,8 @@ use std::sync::{Arc, PoisonError, RwLock};
|
||||
|
||||
use cadcraft_geom::Vec2;
|
||||
|
||||
pub use compile::compile;
|
||||
use craft_shx::Record;
|
||||
pub use craft_shx::{ShxError, ShxKind, compile};
|
||||
pub use table::{
|
||||
FontProvider, add_drawing_dir, clear_registered, drawing_dirs, find, font_map, generation, installed, register, search_path, set_font_dirs,
|
||||
set_font_map, set_provider, set_search_path,
|
||||
@@ -47,46 +47,6 @@ pub use table::{
|
||||
pub use text::ShxText;
|
||||
pub(crate) use text::{shape, width};
|
||||
|
||||
/// Why a font file can't be read or a shape source can't be compiled.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ShxError {
|
||||
/// Not an SHX file (unknown signature).
|
||||
NotShx,
|
||||
/// The file ends inside its header or index.
|
||||
Truncated,
|
||||
/// The file is larger, or has more of something, than CADCraft reads.
|
||||
TooLarge,
|
||||
/// The file is well-formed but holds no shape.
|
||||
Empty,
|
||||
/// Shape source error: line and message.
|
||||
Source(usize, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ShxError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ShxError::NotShx => f.write_str("not an SHX shape or font file"),
|
||||
ShxError::Truncated => f.write_str("the SHX file is truncated"),
|
||||
ShxError::TooLarge => f.write_str("the SHX file is too large"),
|
||||
ShxError::Empty => f.write_str("the SHX file has no shapes"),
|
||||
ShxError::Source(line, msg) => write!(f, "line {line}: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ShxError {}
|
||||
|
||||
/// The kind of SHX file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ShxKind {
|
||||
/// A shape file or a text font with single-byte character codes (`shapes 1.0/1.1`).
|
||||
Shapes,
|
||||
/// A big font: double-byte character codes after an escape (lead) byte.
|
||||
BigFont,
|
||||
/// A Unicode font: shape numbers are Unicode code points.
|
||||
Unifont,
|
||||
}
|
||||
|
||||
/// A shape's program run into polylines, in font units (pen starts at the origin).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ShxGlyph {
|
||||
@@ -96,13 +56,6 @@ pub struct ShxGlyph {
|
||||
pub advance: Vec2,
|
||||
}
|
||||
|
||||
/// One shape record: where its name and program are in [`ShxFont::data`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Record {
|
||||
name: (usize, usize),
|
||||
program: (usize, usize),
|
||||
}
|
||||
|
||||
/// Glyphs cached per font, keyed by (shape, vertical).
|
||||
type GlyphCache = HashMap<(u16, bool), Option<Arc<ShxGlyph>>>;
|
||||
|
||||
|
||||
+21
-220
@@ -1,225 +1,26 @@
|
||||
//! Reading the compiled SHX containers (layouts in the module documentation). Every count and
|
||||
//! offset comes from the file, so each is checked against its length; records that run past
|
||||
//! the end are dropped and the rest kept.
|
||||
//! Bridge the shared bounded container reader into CADCraft's font and glyph cache.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use super::{ShxError, ShxFont};
|
||||
pub(super) use craft_shx::parse::MAX_FILE_BYTES;
|
||||
#[cfg(test)]
|
||||
pub(super) use craft_shx::parse::{BIGFONT, SHAPES_10, UNIFONT};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use super::{Record, ShxError, ShxFont, ShxKind};
|
||||
|
||||
pub(super) const SHAPES_10: &[u8] = b"AutoCAD-86 shapes 1.0\r\n\x1a";
|
||||
pub(super) const SHAPES_11: &[u8] = b"AutoCAD-86 shapes 1.1\r\n\x1a";
|
||||
pub(super) const BIGFONT: &[u8] = b"AutoCAD-86 bigfont 1.0\r\n\x1a";
|
||||
pub(super) const UNIFONT: &[u8] = b"AutoCAD-86 unifont 1.0\r\n\x1a";
|
||||
|
||||
/// Largest file read (the largest big fonts are a few megabytes).
|
||||
pub(super) const MAX_FILE_BYTES: usize = 32 << 20;
|
||||
/// Most escape ranges in a big font (lead bytes: at most 256).
|
||||
const MAX_RANGES: usize = 256;
|
||||
|
||||
/// A checked little-endian reader.
|
||||
struct Rd<'a> {
|
||||
b: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Rd<'a> {
|
||||
fn take(&mut self, n: usize) -> Result<&'a [u8], ShxError> {
|
||||
let end = self.pos.checked_add(n).ok_or(ShxError::Truncated)?;
|
||||
let s = self.b.get(self.pos..end).ok_or(ShxError::Truncated)?;
|
||||
self.pos = end;
|
||||
Ok(s)
|
||||
}
|
||||
fn u16(&mut self) -> Result<u16, ShxError> {
|
||||
match self.take(2)? {
|
||||
[a, b] => Ok(u16::from_le_bytes([*a, *b])),
|
||||
_ => Err(ShxError::Truncated),
|
||||
}
|
||||
}
|
||||
fn u32(&mut self) -> Result<u32, ShxError> {
|
||||
match self.take(4)? {
|
||||
[a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
|
||||
_ => Err(ShxError::Truncated),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A font being read.
|
||||
struct Font {
|
||||
kind: ShxKind,
|
||||
name: String,
|
||||
header: bool,
|
||||
above: f64,
|
||||
below: f64,
|
||||
dual: bool,
|
||||
encoding: u8,
|
||||
char_width: f64,
|
||||
ranges: Vec<(u16, u16)>,
|
||||
records: HashMap<u16, Record>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
fn new(kind: ShxKind) -> Font {
|
||||
Font {
|
||||
kind,
|
||||
name: String::new(),
|
||||
header: false,
|
||||
above: 0.0,
|
||||
below: 0.0,
|
||||
dual: false,
|
||||
encoding: 0,
|
||||
char_width: 0.0,
|
||||
ranges: Vec::new(),
|
||||
records: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the record of shape `number` at `start..end` of `data`: a NUL-terminated name, then
|
||||
/// the program. Shape 0 is the font record. A record without a name terminator is dropped;
|
||||
/// the first record of a number wins.
|
||||
fn record(&mut self, data: &[u8], number: u16, start: usize, end: usize) {
|
||||
let Some(bytes) = data.get(start..end) else { return };
|
||||
let Some(nul) = bytes.iter().position(|b| *b == 0) else { return };
|
||||
let name = (start, start + nul);
|
||||
let program = (start + nul + 1, end);
|
||||
if number == 0 {
|
||||
if !self.header {
|
||||
self.font_record(data.get(program.0..program.1).unwrap_or_default(), data.get(name.0..name.1).unwrap_or_default());
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.records.entry(number).or_insert(Record { name, program });
|
||||
}
|
||||
|
||||
/// The font record: above, below, modes (and for a unifont encoding and embedding); an
|
||||
/// extended big font's is character height, 0, modes, character width.
|
||||
fn font_record(&mut self, info: &[u8], name: &[u8]) {
|
||||
self.header = true;
|
||||
self.name = name.iter().take(255).map(|c| char::from(*c)).collect();
|
||||
let byte = |i: usize| info.get(i).copied().unwrap_or(0);
|
||||
self.above = f64::from(byte(0));
|
||||
self.below = f64::from(byte(1));
|
||||
self.dual = byte(2) == 2;
|
||||
if self.kind == ShxKind::Unifont {
|
||||
self.encoding = byte(3);
|
||||
}
|
||||
if self.kind == ShxKind::BigFont && info.len() >= 5 && byte(1) == 0 {
|
||||
// Extended: height, 0, modes, width, 0.
|
||||
self.below = 0.0;
|
||||
self.char_width = f64::from(byte(3));
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self, data: Vec<u8>) -> Result<ShxFont, ShxError> {
|
||||
if self.records.is_empty() {
|
||||
return Err(ShxError::Empty);
|
||||
}
|
||||
Ok(ShxFont {
|
||||
kind: self.kind,
|
||||
name: self.name,
|
||||
header: self.header,
|
||||
above: self.above,
|
||||
below: self.below,
|
||||
dual: self.dual,
|
||||
encoding: self.encoding,
|
||||
char_width: if self.char_width > 0.0 { self.char_width } else { self.above },
|
||||
ranges: self.ranges,
|
||||
data,
|
||||
records: self.records,
|
||||
cache: RwLock::default(),
|
||||
hebrew: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse(bytes: &[u8]) -> Result<ShxFont, ShxError> {
|
||||
if bytes.len() > MAX_FILE_BYTES {
|
||||
return Err(ShxError::TooLarge);
|
||||
}
|
||||
if bytes.starts_with(SHAPES_10) || bytes.starts_with(SHAPES_11) {
|
||||
shapes(bytes)
|
||||
} else if bytes.starts_with(BIGFONT) {
|
||||
bigfont(bytes)
|
||||
} else if bytes.starts_with(UNIFONT) {
|
||||
unifont(bytes)
|
||||
} else {
|
||||
Err(ShxError::NotShx)
|
||||
}
|
||||
}
|
||||
|
||||
fn shapes(bytes: &[u8]) -> Result<ShxFont, ShxError> {
|
||||
let mut r = Rd { b: bytes, pos: SHAPES_10.len() };
|
||||
let _first = r.u16()?;
|
||||
let _last = r.u16()?;
|
||||
let count = usize::from(r.u16()?);
|
||||
// The whole index must be there before anything is read from it.
|
||||
let index = r.take(count.checked_mul(4).ok_or(ShxError::TooLarge)?)?;
|
||||
let mut f = Font::new(ShxKind::Shapes);
|
||||
let mut pos = r.pos;
|
||||
for [a, b, c, d] in index.as_chunks::<4>().0 {
|
||||
let (number, len) = (u16::from_le_bytes([*a, *b]), usize::from(u16::from_le_bytes([*c, *d])));
|
||||
let Some(end) = pos.checked_add(len).filter(|e| *e <= bytes.len()) else { break };
|
||||
f.record(bytes, number, pos, end);
|
||||
pos = end;
|
||||
}
|
||||
f.finish(bytes.to_vec())
|
||||
}
|
||||
|
||||
fn bigfont(bytes: &[u8]) -> Result<ShxFont, ShxError> {
|
||||
let mut r = Rd { b: bytes, pos: BIGFONT.len() };
|
||||
let index_size = usize::from(r.u16()?);
|
||||
let count = usize::from(r.u16()?);
|
||||
let nranges = usize::from(r.u16()?);
|
||||
if nranges > MAX_RANGES {
|
||||
return Err(ShxError::TooLarge);
|
||||
}
|
||||
let mut f = Font::new(ShxKind::BigFont);
|
||||
for _ in 0..nranges {
|
||||
let (lo, hi) = (r.u16()?, r.u16()?);
|
||||
f.ranges.push((lo.min(hi), lo.max(hi)));
|
||||
}
|
||||
// The index has `count` entries of 8 bytes; a file whose count doesn't fit but whose index
|
||||
// size does is read by that.
|
||||
let fits = |n: usize| n.checked_mul(8).and_then(|l| r.pos.checked_add(l)).is_some_and(|e| e <= bytes.len());
|
||||
let n = if fits(count) {
|
||||
count
|
||||
} else if fits(index_size) {
|
||||
index_size
|
||||
} else {
|
||||
return Err(ShxError::Truncated);
|
||||
};
|
||||
let index = r.take(n.checked_mul(8).ok_or(ShxError::TooLarge)?)?;
|
||||
for [a, b, c, d, o0, o1, o2, o3] in index.as_chunks::<8>().0 {
|
||||
let number = u16::from_le_bytes([*a, *b]);
|
||||
let len = usize::from(u16::from_le_bytes([*c, *d]));
|
||||
let Ok(off) = usize::try_from(u32::from_le_bytes([*o0, *o1, *o2, *o3])) else { continue };
|
||||
if len == 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(end) = off.checked_add(len).filter(|e| *e <= bytes.len()) else { continue };
|
||||
f.record(bytes, number, off, end);
|
||||
}
|
||||
f.finish(bytes.to_vec())
|
||||
}
|
||||
|
||||
fn unifont(bytes: &[u8]) -> Result<ShxFont, ShxError> {
|
||||
let mut r = Rd { b: bytes, pos: UNIFONT.len() };
|
||||
let count = r.u32()?;
|
||||
let info_len = usize::from(r.u16()?);
|
||||
let info = r.take(info_len)?;
|
||||
let mut f = Font::new(ShxKind::Unifont);
|
||||
let nul = info.iter().position(|b| *b == 0).unwrap_or(info.len());
|
||||
f.font_record(info.get(nul.saturating_add(1)..).unwrap_or_default(), info.get(..nul).unwrap_or_default());
|
||||
// `count` includes the font record; never trusted beyond the data that is there.
|
||||
let mut left = count.saturating_sub(1);
|
||||
while left > 0 {
|
||||
left -= 1;
|
||||
let Ok(number) = r.u16() else { break };
|
||||
let Ok(len) = r.u16().map(usize::from) else { break };
|
||||
let start = r.pos;
|
||||
let Some(end) = start.checked_add(len).filter(|e| *e <= bytes.len()) else { break };
|
||||
r.pos = end;
|
||||
f.record(bytes, number, start, end);
|
||||
}
|
||||
f.finish(bytes.to_vec())
|
||||
let font = craft_shx::parse::parse(bytes)?;
|
||||
Ok(ShxFont {
|
||||
kind: font.kind,
|
||||
name: font.name,
|
||||
header: font.header,
|
||||
above: font.above,
|
||||
below: font.below,
|
||||
dual: font.dual,
|
||||
encoding: font.encoding,
|
||||
char_width: font.char_width,
|
||||
ranges: font.ranges,
|
||||
data: font.data,
|
||||
records: font.records,
|
||||
cache: RwLock::default(),
|
||||
hebrew: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-mcp = { workspace = true }
|
||||
craft-jsonrpc = { workspace = true }
|
||||
cadcraft-engine = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
cadcraft-io = { workspace = true }
|
||||
|
||||
+48
-37
@@ -22,24 +22,25 @@ Verify with inspect_drawing / query_entities and look with render.";
|
||||
|
||||
pub struct Server {
|
||||
backend: Box<dyn Backend>,
|
||||
session: craft_mcp::Session,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
fn response(id: Value, result: Value) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "result": result})
|
||||
}
|
||||
fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message.into()}})
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(backend: Box<dyn Backend>) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
session: craft_mcp::Session::new("cadcraft", env!("CARGO_PKG_VERSION"), craft_mcp::Policy::Compatible)
|
||||
.with_capabilities(json!({"tools": {"listChanged": false}, "resources": {"listChanged": false}}))
|
||||
.unwrap_or_else(|_| craft_mcp::Session::new("cadcraft", env!("CARGO_PKG_VERSION"), craft_mcp::Policy::Compatible))
|
||||
.with_legacy_initialize(SUPPORTED, json!({"name": "cadcraft", "title": "CADCraft", "version": env!("CARGO_PKG_VERSION")})),
|
||||
}
|
||||
Self { backend, initialized: false }
|
||||
}
|
||||
pub fn backend(&mut self) -> &mut dyn Backend {
|
||||
self.backend.as_mut()
|
||||
}
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.session.is_initialized()
|
||||
self.initialized
|
||||
}
|
||||
|
||||
/// Serve newline-delimited JSON-RPC until `input` closes (logs to stderr only).
|
||||
@@ -61,43 +62,53 @@ impl Server {
|
||||
return None;
|
||||
}
|
||||
let reply = match serde_json::from_str::<Value>(line) {
|
||||
Ok(msg) => self.handle_message(&msg),
|
||||
Err(e) => Some(craft_jsonrpc::error_response(Value::Null, craft_jsonrpc::RpcError::new(PARSE_ERROR, format!("parse error: {e}")))),
|
||||
// JSON-RPC 2.0: an empty batch is one Invalid Request, not silence.
|
||||
Ok(Value::Array(batch)) if batch.is_empty() => Some(error(Value::Null, INVALID_REQUEST, "empty batch")),
|
||||
Ok(Value::Array(batch)) => {
|
||||
let replies: Vec<Value> = batch.into_iter().filter_map(|m| self.handle(m)).collect();
|
||||
(!replies.is_empty()).then_some(Value::Array(replies))
|
||||
}
|
||||
Ok(msg) => self.handle(msg),
|
||||
Err(e) => Some(error(Value::Null, PARSE_ERROR, format!("parse error: {e}"))),
|
||||
};
|
||||
reply.map(|r| r.to_string())
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, msg: Value) -> Option<Value> {
|
||||
// `handle` historically routes only one envelope; batches belong to `handle_line`.
|
||||
if msg.is_array() {
|
||||
return Some(craft_jsonrpc::error_response(Value::Null, craft_jsonrpc::RpcError::new(INVALID_REQUEST, "message must be an object")));
|
||||
}
|
||||
self.handle_message(&msg)
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, msg: &Value) -> Option<Value> {
|
||||
let server = std::cell::RefCell::new(self);
|
||||
craft_jsonrpc::process_legacy(
|
||||
msg,
|
||||
|request| {
|
||||
let mut server = server.borrow_mut();
|
||||
if request.method == "initialize" {
|
||||
let instructions = format!("{INSTRUCTIONS} Backend: {}.", server.backend.describe());
|
||||
server.session.set_instructions(instructions);
|
||||
}
|
||||
if let Some(result) = server.session.request(request, msg.is_array()) {
|
||||
return result;
|
||||
}
|
||||
server
|
||||
.request(request.method, request.params.unwrap_or(&Value::Null))
|
||||
.map_err(|(code, message)| craft_jsonrpc::RpcError::new(code, message))
|
||||
},
|
||||
|request| server.borrow_mut().session.notification(request),
|
||||
)
|
||||
let Value::Object(o) = &msg else { return Some(error(Value::Null, INVALID_REQUEST, "message must be an object")) };
|
||||
let id = o.get("id").cloned();
|
||||
let Some(method) = o.get("method").and_then(Value::as_str) else {
|
||||
if o.contains_key("result") || o.contains_key("error") {
|
||||
return None;
|
||||
}
|
||||
return Some(error(id.unwrap_or(Value::Null), INVALID_REQUEST, "missing `method`"));
|
||||
};
|
||||
let params = o.get("params").cloned().unwrap_or(Value::Null);
|
||||
let Some(id) = id else {
|
||||
if method == "notifications/initialized" {
|
||||
self.initialized = true;
|
||||
}
|
||||
return None;
|
||||
};
|
||||
Some(match self.request(method, ¶ms) {
|
||||
Ok(r) => response(id, r),
|
||||
Err((code, m)) => error(id, code, m),
|
||||
})
|
||||
}
|
||||
|
||||
fn request(&mut self, method: &str, params: &Value) -> Result<Value, (i64, String)> {
|
||||
match method {
|
||||
"initialize" => {
|
||||
let asked = params.get("protocolVersion").and_then(Value::as_str).unwrap_or(PROTOCOL_VERSION);
|
||||
let version = if SUPPORTED.contains(&asked) { asked } else { PROTOCOL_VERSION };
|
||||
Ok(json!({
|
||||
"protocolVersion": version,
|
||||
"capabilities": {"tools": {"listChanged": false}, "resources": {"listChanged": false}},
|
||||
"serverInfo": {"name": "cadcraft", "title": "CADCraft", "version": env!("CARGO_PKG_VERSION")},
|
||||
"instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()),
|
||||
}))
|
||||
}
|
||||
"ping" => Ok(json!({})),
|
||||
"tools/list" => Ok(tool_definitions(self.backend.has_ui())),
|
||||
"tools/call" => {
|
||||
let name = params.get("name").and_then(Value::as_str).ok_or((INVALID_PARAMS, "missing tool `name`".to_string()))?;
|
||||
|
||||
@@ -391,24 +391,3 @@ fn render_refuses_unknown_backgrounds_and_plot_styles() {
|
||||
assert!(crate::Backend::call(&mut h, "ui.render", json!({"background": "purple"})).is_err());
|
||||
assert!(crate::Backend::call(&mut h, "ui.render", json!({"plot_style": 3})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_protocol_routing_and_version_negotiation() {
|
||||
let mut s = Server::new(Box::new(Headless::default()));
|
||||
assert!(s.handle_line(r#"{"method":"notifications/initialized"}"#).is_none());
|
||||
assert!(s.is_initialized());
|
||||
for (params, expected) in
|
||||
[(json!(null), "2025-06-18"), (json!({"protocolVersion":"2024-11-05"}), "2024-11-05"), (json!({"protocolVersion":"other"}), "2025-06-18")]
|
||||
{
|
||||
let response = call(&mut s, 1, "initialize", params);
|
||||
assert_eq!(response["result"]["protocolVersion"], expected);
|
||||
assert_eq!(response["result"]["serverInfo"]["title"], "CADCraft");
|
||||
assert!(response["result"]["instructions"].as_str().unwrap().contains("Backend:"));
|
||||
}
|
||||
let response: Value =
|
||||
serde_json::from_str(&s.handle_line(r#"[{"id":[1],"method":"ping","params":5},{"method":"ignored"},{"result":1},{}]"#).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
response,
|
||||
json!([{"jsonrpc":"2.0","id":[1],"result":{}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"missing `method`"}}])
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user