Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 59468ec6e5 test: avoid unchecked option parsing in Flatpak regression 2026-11-11 14:08:46 -05:00
Dylan M. Taylor f9dd53a77e Release 0.20.1: fix Flatpak update permissions 2026-11-11 14:04:24 -05:00
FredApps 28241abb8f Merge main into release for CADCraft v0.20.0 2026-11-11 16:38:54 +01:00
FredAppsand.CooCoo 51f5bbcc21 Fonts: the MTEXT strip test expects a short \U+ escape to keep its digits (#2954) (#3024)
strips_formatting expected `\U+12` to strip to nothing. plain_mtext
dropped every unknown code up to the next `;` when that assertion was
written. #1036 limits this to the codes in takes_arg, and `\U` is not
one of them, so plain_mtext returns `+12`. The layout and paragraph
paths give the same text, and
unicode_escapes_are_decoded_in_every_parser already expects it.

The test has failed on main since both changes merged, and the failure
stops the gate at its test step. The test module now also checks the
short escape in both layout paths.

(cherry picked from commit 6f0f1fefa8)

Co-authored-by: .CooCoo <RobertLeeLW@gmail.com>
2026-11-10 19:49:24 +01:00
Dylan M. Taylor 227e3c385b Release 0.19.0 2026-11-09 06:29:44 -05:00
13 changed files with 79 additions and 239 deletions
Generated
+16 -16
View File
@@ -614,7 +614,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadcraft"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -645,7 +645,7 @@ dependencies = [
[[package]]
name = "cadcraft-cli"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -661,14 +661,14 @@ dependencies = [
[[package]]
name = "cadcraft-color"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-constraints"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-doc",
"cadcraft-geom",
@@ -678,7 +678,7 @@ dependencies = [
[[package]]
name = "cadcraft-doc"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-geom",
@@ -689,7 +689,7 @@ dependencies = [
[[package]]
name = "cadcraft-dwg"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"acadrust",
"log",
@@ -697,7 +697,7 @@ dependencies = [
[[package]]
name = "cadcraft-dxf"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"encoding_rs",
"log",
@@ -706,7 +706,7 @@ dependencies = [
[[package]]
name = "cadcraft-engine"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-constraints",
@@ -727,7 +727,7 @@ dependencies = [
[[package]]
name = "cadcraft-fonts"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-dxf",
"cadcraft-geom",
@@ -740,14 +740,14 @@ dependencies = [
[[package]]
name = "cadcraft-geom"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-io"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"acadrust",
"cadcraft-color",
@@ -769,7 +769,7 @@ dependencies = [
[[package]]
name = "cadcraft-mcp"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -784,7 +784,7 @@ dependencies = [
[[package]]
name = "cadcraft-render"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -798,7 +798,7 @@ dependencies = [
[[package]]
name = "cadcraft-ui-egui"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -825,7 +825,7 @@ dependencies = [
[[package]]
name = "cadcraft-web"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-fonts",
@@ -5993,7 +5993,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"chrono",
"serde_json",
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
# default-members
[workspace.package]
version = "0.20.0"
version = "0.20.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+6 -61
View File
@@ -19,21 +19,6 @@ const MAX_TABLE_ROWS: usize = 2000;
const MAX_TABLE_COLS: usize = 200;
/// Attribute columns kept at most.
const MAX_TAGS: usize = 190;
/// Attribute text one extraction may hold across all its references (each row owns a copy of its
/// values, constants included). Far above any real schedule; an error, never a silent cut.
const MAX_VALUE_BYTES: usize = 1 << 30;
/// Add `add` bytes to `used`; an error past `limit`.
fn charge(used: &mut usize, add: usize, limit: usize) -> Result<()> {
*used = used.saturating_add(add);
if *used > limit {
return Err(bad(
"dataextraction",
format!("the attribute text of the block references passes {} MiB; narrow `blocks`, `tags` or `handles`", limit >> 20),
));
}
Ok(())
}
const PARAMS: &str = "{blocks?: \"A*,B\" (block names, wildcards, default *), tags?: attribute tags to include (wildcards, default *), handles? (default: the block references in model space and every layout), combine?: bool (default true: identical rows once, with a Count column; false: one row per reference with handle, layer, position and rotation), file?: path (.csv; .txt is tab-separated), at?: [x,y] (insert a table there), title?: table title} → {columns, rows, file?, table?}";
@@ -182,8 +167,6 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
};
let selected = selected_tags(&ents, d, o);
let mut out = Vec::new();
let mut used = 0usize;
let mut const_sizes: HashMap<String, Vec<(String, usize)>> = HashMap::new();
let mut consts: HashMap<String, Vec<(String, String)>> = HashMap::new();
for e in &ents {
let EntityKind::Insert(ins) = &e.kind else { continue };
@@ -192,47 +175,18 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
continue;
}
let block = d.block(&ins.block).map(|b| b.name.clone()).unwrap_or_else(|| ins.block.clone());
// Charged from the lengths, before any text is copied.
let selected_bytes: usize =
ins.attribs.iter().filter(|a| selected.contains(&a.tag.to_ascii_uppercase())).map(|a| a.text.value.len() + a.tag.len()).sum();
// The constants this row copies (those its own attributes do not override), from the lengths.
let first_of_block = !const_sizes.contains_key(&block);
let consts_sizes = const_sizes.entry(block.clone()).or_insert_with(|| {
d.block(&ins.block).map_or_else(Vec::new, |b| {
b.entities
.iter()
.filter_map(|e| match &e.kind {
EntityKind::AttDef(a) if a.constant && selected.iter().any(|t| t == &a.tag.to_ascii_uppercase()) => {
Some((a.tag.to_ascii_uppercase(), a.tag.len() + a.text.value.len()))
}
_ => None,
})
.collect()
})
});
if first_of_block {
// The per-block cache of constants is one more copy of them, made once.
charge(&mut used, consts_sizes.iter().map(|(_, n)| *n).fold(0, usize::saturating_add), MAX_VALUE_BYTES)?;
}
let const_bytes: usize = consts_sizes
.iter()
.filter(|(tag, _)| !ins.attribs.iter().any(|a| a.tag.eq_ignore_ascii_case(tag)))
.map(|(_, n)| *n)
.fold(0, usize::saturating_add);
charge(&mut used, selected_bytes.saturating_add(const_bytes), MAX_VALUE_BYTES)?;
let values = values(
ins,
// Keyed by the resolved definition's exact name: `Part` and `PART` can both exist.
consts.entry(block.clone()).or_insert_with(|| d.block(&ins.block).map(|b| constants(b, &selected)).unwrap_or_default()),
&selected,
);
out.push(Found {
handle: e.handle.hex(),
layer: e.common.layer.clone(),
at: ins.insert.xy(),
rotation: ins.rotation.to_degrees(),
block: block.clone(),
values,
values: values(
ins,
// Keyed by the resolved definition's exact name: `Part` and `PART` can both exist.
consts.entry(block.clone()).or_insert_with(|| d.block(&ins.block).map(|b| constants(b, &selected)).unwrap_or_default()),
&selected,
),
});
}
Ok(out)
@@ -495,15 +449,6 @@ impl Interactive for ExtractM {
#[cfg(test)]
mod tests {
#[test]
fn attribute_text_is_charged_against_one_budget() {
let mut used = 0;
assert!(super::charge(&mut used, 60, 100).is_ok());
assert!(super::charge(&mut used, 40, 100).is_ok());
assert!(super::charge(&mut used, 1, 100).is_err());
assert!(super::charge(&mut used, usize::MAX, 100).is_err());
}
use super::*;
#[test]
+13 -40
View File
@@ -6,7 +6,7 @@
//! of nested xrefs (`xref|nested|name`) bind with every `|` written `$0$`.
use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use cadcraft_doc::{Drawing, EntityStore, MAX_BLOCK_DEPTH, XrefStatus};
@@ -20,16 +20,6 @@ fn split(name: &str) -> Option<(&str, &str)> {
/// Whether table `sym` of `d` has a symbol called `name`.
fn taken(d: &Drawing, sym: Sym, name: &str) -> bool {
taken_with(d, sym, name, None)
}
/// [`taken`], answering for blocks from `blocks` (the upper-cased names of `d`'s blocks, kept
/// current by the caller) when given: a lookup that misses scans every block, which a bind
/// naming thousands of blocks would repeat for each.
fn taken_with(d: &Drawing, sym: Sym, name: &str, blocks: Option<&HashSet<String>>) -> bool {
if let (Sym::Block, Some(set)) = (sym, blocks) {
return set.contains(&name.to_ascii_uppercase());
}
match sym {
Sym::Layer => d.layer(name).is_some(),
Sym::Linetype => d.linetype(name).is_some(),
@@ -42,12 +32,12 @@ fn taken_with(d: &Drawing, sym: Sym, name: &str, blocks: Option<&HashSet<String>
}
/// The bound name of `key|rest`: `key$n$rest` with the first free `n`, or `rest` (Insert type).
fn bound_name(d: &Drawing, sym: Sym, key: &str, rest: &str, insert: bool, blocks: Option<&HashSet<String>>) -> String {
fn bound_name(d: &Drawing, sym: Sym, key: &str, rest: &str, insert: bool) -> String {
let rest = rest.replace('|', "$0$");
if insert {
return rest;
}
(0..10_000).map(|n| format!("{key}${n}${rest}")).find(|c| !taken_with(d, sym, c, blocks)).unwrap_or_else(|| format!("{key}$${rest}"))
(0..10_000).map(|n| format!("{key}${n}${rest}")).find(|c| !taken(d, sym, c)).unwrap_or_else(|| format!("{key}$${rest}"))
}
/// Every table's symbol names, for one table at a time.
@@ -63,8 +53,9 @@ fn names(d: &Drawing, sym: Sym) -> Vec<String> {
}
}
/// Rename symbol `old` of table `sym` to `new`, or drop it (`merge`: `new` already exists, Insert type).
fn rename_symbol(d: &mut Drawing, sym: Sym, old: &str, new: &str, merge: bool) {
/// Rename symbol `old` of table `sym` to `new`, or drop it (`new` already exists: Insert type).
fn rename_symbol(d: &mut Drawing, sym: Sym, old: &str, new: &str) {
let merge = !new.eq_ignore_ascii_case(old) && taken(d, sym, new);
macro_rules! table {
($v:expr) => {{
if merge {
@@ -110,17 +101,12 @@ pub(crate) fn bind(d: &mut Drawing, key: &str, insert: bool) -> Result<(), Strin
let mut map: HashMap<(Sym, String), String> = HashMap::new();
let mut bound_blocks = vec![key.clone()];
for sym in TABLES {
let mut blocks: Option<HashSet<String>> = (sym == Sym::Block).then(|| d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect());
for old in names(d, sym).into_iter().filter(|n| has_prefix(n, &prefix)) {
let rest = old.get(prefix.len()..).unwrap_or_default();
let new = bound_name(d, sym, &key, rest, insert, blocks.as_ref());
let new = bound_name(d, sym, &key, rest, insert);
// Insert type: a host symbol of the bound name is used instead of the xref's.
let merged = taken_with(d, sym, &new, blocks.as_ref());
rename_symbol(d, sym, &old, &new, !new.eq_ignore_ascii_case(&old) && merged);
if let Some(set) = &mut blocks {
set.remove(&old.to_ascii_uppercase());
set.insert(new.to_ascii_uppercase());
}
let merged = taken(d, sym, &new);
rename_symbol(d, sym, &old, &new);
if sym == Sym::Block && !merged {
bound_blocks.push(new.clone());
}
@@ -256,27 +242,14 @@ fn xbind_in(d: &mut Drawing, sym: Sym, name: &str, depth: usize) -> Result<Strin
mod tests {
use super::*;
#[test]
fn a_block_name_set_answers_as_the_scan_does() {
let mut d = Drawing::new_imperial();
for n in ["SITE$0$Plan", "Other"] {
d.blocks.insert(n.into(), Arc::new(cadcraft_doc::Block::new(n)));
}
let set: HashSet<String> = d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect();
for n in ["site$0$plan", "SITE$0$Plan", "OTHER", "SITE$1$Plan", "nothing"] {
assert_eq!(taken_with(&d, Sym::Block, n, Some(&set)), taken(&d, Sym::Block, n), "{n}");
}
assert_eq!(bound_name(&d, Sym::Block, "SITE", "plan", false, Some(&set)), "SITE$1$plan");
}
#[test]
fn bound_names() {
let mut d = Drawing::new_imperial();
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", false, None), "SITE$0$Walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", false), "SITE$0$Walls");
d.ensure_layer("SITE$0$Walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "walls", false, None), "SITE$1$walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", true, None), "Walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "TREES|Oak", false, None), "SITE$0$TREES$0$Oak");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "walls", false), "SITE$1$walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", true), "Walls");
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "TREES|Oak", false), "SITE$0$TREES$0$Oak");
assert_eq!(split("A|b|c"), Some(("A", "b|c")));
assert_eq!(split("|b"), None);
}
-38
View File
@@ -497,30 +497,6 @@ fn frame_books(d: &Drawing, frame_height: f64, unit: f64) -> (Vec<Vec<Frame>>, u
(books, skipped)
}
/// Most page-by-entity renderings one PDFBOOK may ask for: every page draws the whole model through
/// its window, so the work is the frames times the model's objects (1,000 sheets of a 500,000-object
/// drawing).
const MAX_BOOK_WORK: u64 = 500_000_000;
/// Objects one page draws: the model's objects, and the contents of each block reference (once per
/// array cell, and the reference's own attributes; nested blocks count as their own entries).
fn model_visits(d: &Drawing) -> usize {
d.model
.iter()
.map(|e| match &e.kind {
EntityKind::Insert(i) => {
d.block(&i.block).map_or(1, |b| 1 + b.entities.len().saturating_mul((i.rows as usize).saturating_mul(i.cols as usize)))
}
_ => 1,
})
.fold(0usize, usize::saturating_add)
}
/// Whether `frames` pages of a model with `entities` objects stay within [`MAX_BOOK_WORK`].
fn book_work_fits(frames: usize, entities: usize) -> bool {
(frames as u64).saturating_mul(entities as u64) <= MAX_BOOK_WORK
}
fn run_pdfbook(s: &mut Session, p: &Value) -> Result<Value> {
if io().and_then(|h| h.plot).is_none() {
return Err(bad("pdfbook", "plotting is not available in this build"));
@@ -531,13 +507,6 @@ fn run_pdfbook(s: &mut Session, p: &Value) -> Result<Value> {
if books.is_empty() {
return Err(bad("pdfbook", format!("no closed polylines {frame_height} mm high (sheet frames) in model space")));
}
let sheets: usize = books.iter().map(Vec::len).sum();
if !book_work_fits(sheets, model_visits(s.doc()?)) {
return Err(bad(
"pdfbook",
format!("{sheets} sheets of a model this large are too much work for one command; plot part of the drawing at a time"),
));
}
let target = str_param(p, "path").map(str::to_string).or_else(|| s.state().ok().and_then(|st| st.path.clone()));
let pb = target.as_deref().map(std::path::Path::new);
let dir = pb.and_then(std::path::Path::parent).map(std::path::Path::to_path_buf).unwrap_or_default();
@@ -592,13 +561,6 @@ mod tests {
Ok(())
}
#[test]
fn a_book_is_bounded_by_pages_times_objects() {
assert!(book_work_fits(1_000, 500_000));
assert!(!book_work_fits(100_000, 100_000));
assert!(!book_work_fits(usize::MAX, usize::MAX));
}
#[test]
fn web_export_delivers_a_download_instead_of_writing_a_path() {
deliver_file("export", "Drawing1.svg", b"<svg/>", Some(record)).unwrap();
+9 -28
View File
@@ -7,32 +7,16 @@ use super::{
// NCOPY
// =====================================================================================
/// Block entities one NCOPY may visit in all, leaves or not: references to shared, empty or
/// repeated sub-blocks multiply the visits (not the leaves) with each level of nesting.
const MAX_VISITS: usize = 1_000_000;
/// The leaf objects of a block reference in world coordinates (nested references expanded). An
/// error when the nesting needs more than [`MAX_VISITS`] visits.
fn nested_leaves(
d: &cadcraft_doc::Drawing,
ins: &cadcraft_doc::Insert,
m: &Mat3,
depth: usize,
visits: &mut usize,
out: &mut Vec<(Common, EntityKind)>,
) -> Result<()> {
/// The leaf objects of a block reference in world coordinates (nested references expanded).
fn nested_leaves(d: &cadcraft_doc::Drawing, ins: &cadcraft_doc::Insert, m: &Mat3, depth: usize, out: &mut Vec<(Common, EntityKind)>) {
if depth >= MAX_BLOCK_DEPTH || out.len() >= MAX_GEN {
return Ok(());
return;
}
let Some(b) = d.block(&ins.block) else { return Ok(()) };
let Some(b) = d.block(&ins.block) else { return };
let mm = m.then_before(ins.transform(b.base.xy()));
for e in b.entities.iter() {
*visits += 1;
if *visits > MAX_VISITS {
return Err(other("The block reference nests too many objects to copy."));
}
match &e.kind {
EntityKind::Insert(inner) => nested_leaves(d, inner, &mm, depth + 1, visits, out)?,
EntityKind::Insert(inner) => nested_leaves(d, inner, &mm, depth + 1, out),
EntityKind::AttDef(_) => {}
k => {
let mut k = k.clone();
@@ -41,14 +25,13 @@ fn nested_leaves(
}
}
}
Ok(())
}
fn leaves_of(s: &Session, h: Handle, visits: &mut usize) -> Result<Vec<(Common, EntityKind)>> {
fn leaves_of(s: &Session, h: Handle) -> Result<Vec<(Common, EntityKind)>> {
let e = curves::entity(s, h)?;
let EntityKind::Insert(ins) = &e.kind else { return Err(other("Select a block reference.")) };
let mut out = Vec::new();
nested_leaves(s.doc()?, ins, &Mat3::IDENTITY, 0, visits, &mut out)?;
nested_leaves(s.doc()?, ins, &Mat3::IDENTITY, 0, &mut out);
Ok(out)
}
@@ -70,7 +53,7 @@ fn place_copies(s: &mut Session, items: &[(Common, EntityKind)], delta: Vec2) ->
pub(super) fn run_ncopy(s: &mut Session, p: &Value) -> Result<Value> {
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("ncopy", "`handle` (block reference) is required"))?;
let leaves = leaves_of(s, h, &mut 0)?;
let leaves = leaves_of(s, h)?;
let items: Vec<(Common, EntityKind)> = match point_param(p, "pick") {
Some(q) => nearest_leaf(leaves, q).into_iter().collect(),
None => leaves,
@@ -91,8 +74,6 @@ pub(super) fn run_ncopy(s: &mut Session, p: &Value) -> Result<Value> {
pub(super) struct NcopyM {
items: Vec<(Common, EntityKind)>,
selecting_done: bool,
/// Block entities visited by every selection so far: one allowance for the whole command.
visits: usize,
base: Option<Vec2>,
}
@@ -116,7 +97,7 @@ impl Interactive for NcopyM {
return match i {
Input::Point(p) => {
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
let leaf = nearest_leaf(leaves_of(s, h, &mut self.visits)?, p).ok_or_else(|| other("Nothing nested there."))?;
let leaf = nearest_leaf(leaves_of(s, h)?, p).ok_or_else(|| other("Nothing nested there."))?;
s.echo(format!("Nested object: {}", leaf.1.type_name()));
if self.items.len() < MAX_GEN {
self.items.push(leaf);
-22
View File
@@ -777,25 +777,3 @@ fn offset_elliptical_arc_follows_picked_side() {
let outer = max_x(json!([20, 20]));
assert!((outer - 11.0).abs() < 0.05, "outer side went inward: max x = {outer}");
}
/// Nested references multiply visits even when no leaf results; NCOPY refuses past its visit budget.
#[test]
fn ncopy_charges_every_nested_visit() {
use cadcraft_doc::{Block, Common, Entity, Insert};
let mut s = Session::new();
let d = s.doc_mut().unwrap();
let mut n = 1;
for level in 0..15 {
let mut b = Block::new(&format!("B{level}"));
for _ in 0..100 {
let ins = Insert { block: format!("B{}", level + 1), ..Insert::default() };
b.entities.push(Entity::new(Handle(n), EntityKind::Insert(ins)));
n += 1;
}
d.blocks.insert(b.name.clone(), std::sync::Arc::new(b));
}
d.blocks.insert("B15".into(), std::sync::Arc::new(Block::new("B15")));
let h = d.add(&Space::Model, Common::default(), EntityKind::Insert(Insert { block: "B0".into(), ..Insert::default() })).unwrap();
let err = s.execute("ncopy", &json!({ "handle": h.hex() })).unwrap_err().to_string();
assert!(err.contains("nests too many objects"), "{err}");
}
+1 -1
View File
@@ -697,7 +697,7 @@ impl Session {
}
pub(crate) use drawing::from_json as drawing_from_json;
pub(crate) use drawing::{load_source, support_file};
pub(crate) use drawing::load_source;
impl Session {
/// A file APPLOAD loaded, by its name (with or without `.lsp`, any case).
+1 -2
View File
@@ -235,8 +235,7 @@ pub(super) fn autoload(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispErr
return Err(LispError::bad_arg("command name", &cmd));
}
// The stub forgets itself, loads the file and calls what it defined.
// A file in the support folder is loaded from there, never from a same-named file of the working folder.
let file_lit = to_text(&Value::Str(crate::lisp::support_file(ctx.s, &file).map_or_else(|| file.clone(), |p| p.into())), true);
let file_lit = to_text(&Value::Str(file.clone()), true);
let src = format!(
"(lambda () (setq C:{name} nil) (load {file_lit}) (if C:{name} (C:{name}) (princ \"\\n; error: {} did not define {name}\")) (princ))",
file.replace(['"', '\\'], "/")
+1 -30
View File
@@ -1128,9 +1128,7 @@ pub fn trans(_ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
/// What `(load name)` reads: a file loaded through APPLOAD (the web), or one on disk, with
/// `.lsp` added when the name has no extension.
pub fn load_source(s: &Session, name: &str) -> Option<String> {
// An absolute path names a file, not an APPLOAD entry that happens to share its base name.
let absolute = std::path::Path::new(name).is_absolute();
if !absolute && let Some(t) = s.lisp_file(name) {
if let Some(t) = s.lisp_file(name) {
return Some(t);
}
let candidates =
@@ -1141,37 +1139,10 @@ pub fn load_source(s: &Session, name: &str) -> Option<String> {
candidates.iter().chain(&support).find_map(super::builtins::read_text_file)
}
/// The file `name` names in the support folder, when it is there: what an autoload registration
/// loads, so a same-named file in the working folder cannot take its place. `None` for an absolute
/// path, no support folder or no such file there (the general search then applies, as before).
pub fn support_file(s: &Session, name: &str) -> Option<String> {
if std::path::Path::new(name).is_absolute() {
return None;
}
let dir = s.lisp_support_dir()?;
let candidates =
if std::path::Path::new(name).extension().is_none() { vec![format!("{name}.lsp"), name.to_string()] } else { vec![name.to_string()] };
candidates.iter().map(|c| dir.join(c)).find(|p| p.is_file()).map(|p| std::path::absolute(&p).unwrap_or(p).to_string_lossy().into_owned())
}
#[cfg(test)]
mod budget_tests {
use super::*;
#[test]
fn autoload_prefers_the_support_folder_file() {
let dir = std::env::temp_dir().join(format!("cadcraft-lisp-support-file-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("lazy.lsp"), "").unwrap();
let mut s = Session::new();
assert_eq!(support_file(&s, "lazy"), None);
s.set_lisp_support_dir(Some(dir.clone()));
assert_eq!(support_file(&s, "lazy"), Some(dir.join("lazy.lsp").to_string_lossy().into_owned()));
assert_eq!(support_file(&s, "other"), None);
assert_eq!(support_file(&s, dir.join("lazy.lsp").to_str().unwrap()), None);
let _ = std::fs::remove_dir_all(&dir);
}
/// One 64 KB string occurring many times is charged by its length each time.
#[test]
fn shared_strings_spend_the_conversion_budgets() {
+27
View File
@@ -0,0 +1,27 @@
//! Release and source Flatpaks must allow release checks and host update helpers.
fn finish_args(manifest: &str) -> Vec<&str> {
manifest
.split("finish-args:")
.nth(1)
.unwrap_or_default()
.lines()
.take_while(|line| line.is_empty() || line.starts_with(' ') || line.starts_with('#'))
.filter_map(|line| line.trim().strip_prefix("- "))
.map(|arg| arg.trim_matches(['\'', '"']))
.collect()
}
#[test]
fn flatpak_update_permissions() {
let manifests = [
include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml"),
include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.yml"),
];
let args = manifests.map(finish_args);
assert_eq!(args[0], args[1], "source and release permissions must agree");
for permissions in args {
for required in ["--share=network", "--talk-name=org.freedesktop.Flatpak"] {
assert!(permissions.contains(&required), "missing update permission: {required}");
}
}
}
@@ -17,6 +17,8 @@ runtime-version: '26.08'
sdk: org.freedesktop.Sdk
command: cadcraft
finish-args:
# The shared updater inspects and updates this installation via flatpak-spawn --host.
- --talk-name=org.freedesktop.Flatpak
- --filesystem=xdg-data/ArtCraft/versions:ro
# Display: Wayland first; X11 only when no Wayland compositor is running.
- --socket=wayland
@@ -18,6 +18,8 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: cadcraft
finish-args:
# The shared updater inspects and updates this installation via flatpak-spawn --host.
- --talk-name=org.freedesktop.Flatpak
- --filesystem=xdg-data/ArtCraft/versions:ro
# Display: Wayland first; X11 only when no Wayland compositor is running.
- --socket=wayland