Found while testing crates/engine/src/lisp/builtins.rs:798-800 (glob_inner depth limit; depth is incremented per '*' at line 808)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
CECOLOR now reads "BYLAYER", "BYBLOCK", the index as text ("1" after
"red") or "RGB:r,g,b" for a true colour, from getvar, LISP and the
SETVAR ? list, where it was the ACI number (256, 0, 1, or the nearest
index of a true colour). The header still keeps the index and
CECOLOR_RGB, so DXF output and new objects are unchanged.
SETVAR CECOLOR takes "RGB:r,g,b" too, so a value saved with getvar
can be set back, true colour included.
* appload: load a .lsp file from a path or the file picker (#1820)
APPLOAD had no user interface: typed or chosen from Tools > Load Application it
started a command-line prompt instead of a file browser, and the JSON form did
not say which commands the file defined.
- The desktop's APPLOAD opens an rfd file browser filtered to .lsp, starting in
the folder the last routine came from (kept in the preferences as lispDir).
A cancelled picker is a no-op; the web (no native dialog) keeps the engine's
name prompt.
- The JSON form `appload {path}` loads to the end, never opens a dialog and now
returns `commands`, the `c:` commands the file defined; a non-.lsp path is
refused and unreadable or unparsable files return an ordinary error.
- Tests: a synthetic .lsp in a temp folder loads by path and its command runs;
the picker path, its cancelled answer and the remembered folder; refused,
missing and broken files.
* appload: load a picked file interactively; keep path whitespace; web hint
Reviewer follow-ups on #1820:
- A picked file now loads through the interactive APPLOAD command (started
and answered with the picked path, as a typed name is), so a routine it
starts that calls getpoint or another get… waits for the user instead of
receiving nil. JSON calls keep the blocking run.
- A JSON `path` is read as given: only a path of blanks is refused, so a
relative name with leading or trailing spaces is not trimmed into another
file.
- The web has no file system, so APPLOAD no longer asks for a file name it
cannot read: it says to drop a .lsp file on the window, which loads the
file's contents. The host declares this through Services::no_file_system.
Tests: a picked file whose load asks for a point waits for it; a relative
path in a folder whose name starts with a space loads; on a host without a
file system APPLOAD gives the drop hint instead of prompting.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Make LISP angtof honour its units argument
Found while testing crates/engine/src/lisp/drawing.rs:962-966
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* angtof: a units argument outside 0..=4 gives nil instead of reading degrees (CodeRabbit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/engine/src/lisp/input.rs:256-258 (lisp_angbase, used at :127 and :183); cause is crates/engine/src/sysvars.rs:141
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* AutoLISP: entmake defines blocks with BLOCK ... ENDBLK (fixes#1662)
A BLOCK list wasn't an entity, so entmake returned nil and the entities
after it went into model space. Now a BLOCK list begins a definition
(name, flags, base point, description), the entities made meanwhile go
into it, and ENDBLK adds the block (redefining one of the same name, or
naming an anonymous *U block) and returns its name. An open definition,
an xref, an invalid name or a block that inserts itself is refused with
nil, using BLOCK's own name and self-reference checks.
* AutoLISP entmake blocks: review fixes (anonymous names keep a star, no redefinition during REFEDIT, a failed routine drops its open definition)
An anonymous block (flag 1) named without a * got a name like FO1; it now
gets *U. ENDBLK refuses to redefine a block REFEDIT is editing, as BLOCK
does. A routine that fails drops the definition it began, so the next
routine's entities go into the drawing; one that ends normally still
leaves it open.
* AutoLISP entmake blocks: refuse an anonymous block that inserts itself
The self-reference check ran only for named blocks, so an anonymous *U
block holding an INSERT of the name it was about to get (*U1 in an empty
drawing) was stored. The check now runs after the anonymous name is
given, for both kinds.
* AutoLISP entmake blocks: create a definition's layers only when the block is added
Each pending entity created its layer at once, so a definition ENDBLK
refused (a self-reference, REFEDIT) or a failed routine dropped left an
orphan layer. end_block now creates the layers after its checks pass,
just before it adds the block.
* Report DWGPREFIX, DWGTITLED, FILEDIA, CMDECHO and more from GETVAR (#1715)
* Keep CMDECHO and FILEDIA in session settings so they survive drawing switches (#1715)
* Leave LOGINNAME out of GETVAR so scripts and agents can't read the user's login name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security: bound Lisp callbacks, native comparisons, and local file reads (CWE-400, CWE-407)
The command and scripting engine had several unbounded paths a supplied LISP
routine or a selected path could exploit to monopolise the thread or exhaust
memory:
- `call_sync` ran synchronous callbacks (vl-sort comparators, vl-remove-if
predicates, …) with `u64::MAX` fuel, so a constant-memory infinite loop never
yielded and bypassed the blocking execution cap. It now drives the callback in
bounded slices and aborts with an error past a hard step cap (row 2).
- `wcmatch`/`glob` explored `*` placements with only a recursion-depth guard,
and `equal` re-traversed shared cons subtrees. Both now charge every step
against a shared work budget and stop when it is exhausted (row 33).
- `setvar` converted a LISP value to JSON unboundedly, so a shared cons DAG
expanded into gigabytes of owned arrays. Conversion is now depth- and
element-bounded and rejects over-budget values with a LISP error (row 39).
- OPEN/LOAD/APPLOAD (`read_text_file`), SCRIPT, and native open (`read_file`,
`run_open`) buffered a path with `std::fs::read`, so an endless device
exhausted memory before any size check. Reads now go through a new
`capped_read` helper that rejects non-regular files and caps bytes
(rows 21, 34).
New tests cover the callback loop, the oversized setvar structure, and the
capped reader. Addresses findings rows 2, 21, 33, 34, 39.
* fix: bound native Lisp scans and nonblocking file reads
* fix: enforce callback fuel before each execution slice
---------
Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
The pick-box heuristic read the message for a leading "Select", and
this branch had widened it past leading whitespace so that a routine's
own message would be read the same way. That makes a plain point prompt
look like a pick: (getpoint "\nSelect a point: ") showed the pick box
and lost its point modifiers, since pointmod refuses them wherever
picks_objects holds.
A routine writes all of its own message (raw), and says through
picks_one when it picks an object, so the wording needs reading only
where the engine wrote it. Covered for both spellings a routine might
use, with and without the leading newline.
Whether the cursor shows the pick box came from `Prompt::picks_objects()`,
which read the prompt's wording: a selection-set prompt, or a point prompt whose
message starts with "Select". An `entsel`/`nentsel` prompt takes a point and is
not a selection set, so it fell to the wording -- and an AutoLISP routine writes
its own message, conventionally starting with a newline:
(nentsel) "Select object: " pick box
(nentsel "\nSelect second line or polyline: ") starts with "\n" none
(entsel "\nPick a line: ") says no such word none
A prompt now says that it picks one object rather than being read for it, and
`GetRequest::prompt()` sets that for `GetKind::EntSel`, so all three show the
pick box. The wording test is read past leading whitespace as well, so a command
machine's prompt beginning with a newline is not caught by the same thing.
Point modifiers (`.x`, `.y`…) are offered at a point prompt that is not a pick,
so they no longer appear at an `entsel` prompt with a message -- as they never
did at one without.
Fix: #1598
`(= (type x) 'LIST)` is how an AutoLISP routine tests a type, because `type`
returns a symbol. `=` coerced both of its arguments to numbers, so the symbol
failed the coercion and the routine stopped at its first type test:
_$ (= (type 1) 'INT)
bad argument type: numberp: INT
`=` and `/=` now compare numbers and strings as before, and any other atom -- a
symbol, `nil`, an entity name, a selection set, a file handle -- by what it is,
the way `eq` does. Two consequences, both closer to AutoLISP than raising was:
a mismatch of kinds answers `nil` rather than failing, so `(= 1 "1")` and
`(= '(1) '(1))` answer (comparing list structure is `equal`'s job), and
`(= nil nil)` is `T`.
The ordering comparisons still take numbers and strings only, as AutoLISP has
them: `(< 'a 'b)` remains `bad argument type: numberp: A`.
Fix: #1597
* AutoLISP: load and run .lsp routines (#758)
CADCraft runs AutoLISP routines. `.lsp` files load with APPLOAD, (load)
or by dropping them on the window, and at startup from the support folder
(acad.lsp once per session or per drawing with ACADLSPASDOC, acaddoc.lsp
per drawing, then S::STARTUP; never from a drawing's own folder).
(defun c:NAME …) defines commands, (autoload) defers loading to first
use, and (expression) and !variable work at the command line and at
prompts. docs/autolisp.md lists what is supported.
The interpreter (crates/engine/src/lisp) reads source with its own
stack and evaluates on an explicit-stack machine, so a routine can stop
for input (getpoint, pause in command) and carry on later on one thread,
deep recursion is an error rather than a crash, and long loops run a
slice per frame (Esc stops them). It follows AutoLISP: 32-bit wrapping
integers, dynamic scope, *error*, vl-catch-all-apply, AutoLISP's error
wording. About 200 built-ins cover the language, command (with pause and
commands left open), entity lists (entget/entmake/entmod/entdel/entlast/
entnext/handent, through the DXF reader and writer's groups, exposed by
cadcraft-io and handed to the engine through a hook), ssget with modes
and filters, tables, system variables, get… input with initget, and
units. A routine is one undo step; each drawing has its own symbols.
Also: keywords typed as _KEYWORD match at prompts; TEXT run by a
routine's command takes one line; DIMENSION records carry their
measurement (42); Prompt::raw shows a get… prompt as written; the
interface asks Session::prompting() (a command's or a routine's prompt)
where it asked whether a command runs, so clicks, right-click and Delete
work at a routine's prompts; getcorner and based get… prompts drag a
rubber band.
Tests: the language, errors, startup files, interactive input, hostile
input (lisp/tests.rs); the #648 corpus, 18/18, when CADCRAFT_LISP_CORPUS
points at it (tests/lisp_corpus.rs); drop, frames and canvas clicks in
the app (ui-egui/tests/lisp_files.rs).
* AutoLISP: bound memory, drop nested values iteratively, guard the per-frame tick
- Cons cells drop with a work list over both car and cdr (and function
bodies), so a list nested a million deep in its car no longer overflows
the stack when it goes away.
- Memory caps so a runaway routine fails with a LISP error instead of an
uncatchable out-of-memory abort: at most 5,000,000 live cons cells
(counted in Value::cons / Drop, checked every step; cells the drawing's
variables already held at the run's start don't count against it), no
string a built-in makes longer than 8 MiB (strcat checks before joining,
printed text is cut), and a 2 GiB per-run budget for new strings,
printed text and files read.
- Session::lisp_tick runs under catch_unwind, and so does every slice of
the LISP machine: a panic ends the routine with "internal error", puts
the drawing back and leaves the session usable.
- Regression tests for each (deep car nesting, nested functions, the cell
cap, string length and budget, a panicking built-in in a blocking run
and in a ticked one).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>