Skip to content

fix(profiles): resolve inherits by declared name, emit filament_vendor, log degrade paths - #3

Merged
jappyjan merged 1 commit into
bambuddy/profile-resolverfrom
bambuddy/inherits-index
Aug 4, 2026
Merged

fix(profiles): resolve inherits by declared name, emit filament_vendor, log degrade paths#3
jappyjan merged 1 commit into
bambuddy/profile-resolverfrom
bambuddy/inherits-index

Conversation

@jappyjan

@jappyjan jappyjan commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Three follow-ups found while reviewing #2, bundled into one PR because sidecar changes cost an image rebuild and rebuilding twice for three small changes is waste. Bambuddy ticket AFKFelix#64.

1. Slash-named parents silently dropped the whole inherits chain

Some bundled profiles carry a literal / in their name, while the file on disk sanitizes it. resolveProfile derived the parent's path from the inherits string, so the / was read as a directory separator, the read ENOENTed, and the resolver silently deleted inherits and stopped — discarding every remaining ancestor without a word.

This was never just a listing bug: profile-resolver.ts is shared with the slice path and /resolved-process, so a user preset inheriting a slash-named base lost its entire ancestor chain and sliced under-specified, silently. Pre-existing; not introduced by #2.

What the upstream trees actually contain

Grepped from read-only clones of SoftFever/OrcaSlicer@v2.3.2 and bambulab/BambuStudio@v02.07.01.57 — not from an image:

profiles whose inherits contains / distinct parent targets inherits containing ..
OrcaSlicer 40 7 0
BambuStudio 35 2 0

BambuStudio is the two known Bambu families. OrcaSlicer is not — it has five more, all QIDI, which is what settled the design question:

"QIDI Support For PET/PA@Q2-Series"  -> QIDI Support For PET-PA @Q2.json
"QIDI PC/ABS-FR@X-Max 4-Series"      -> QIDI PC-ABS-FR @X-Max 4.json
"Bambu Support For PA/PET @base"     -> Bambu Support For PA PET @base.json
"Bambu Support For PLA/PETG @base"   -> Bambu Support For PLA-PETG @base.json

Four different mappings, and the QIDI ones are not sanitizations at all — @Q2-Series becomes @Q2 and a space appears before the @. No sanitizing path.join can work, because the filename is not a pure function of the declared name. So this indexes the directory by each file's declared name, as the ticket preferred.

Within each BBL/<category> directory — the resolver's actual index scope — there are zero declared-name collisions in either slicer (Orca 1071 names / 1072 files, BambuStudio 1723 / 1727), so the index is unambiguous.

Design: lazy-on-miss

The direct path.join read is tried first and the index is built only when it misses, memoised per category directory. The hot path stays exactly one readFile per hop, so the ~2500-file listing loop pays no directory enumeration when nothing is slash-named. Two unit tests pin that contract (readdir called once across five slash resolutions; never when the direct path hits).

Path traversal closed structurally

The direct lookup now refuses anything that is not a plain basename (/, \, \0, ., .., absolute), and the index can only ever name files readdir produced from inside the category directory. There is no longer a path by which an inherits value reaches outside the profiles tree. No shipped profile exercises this (0 occurrences of .. in either tree) — it is hardening, not an active exploit.

2. filament_vendor

One hop up the chain the walk already traverses — ["Bambu Lab"] on Bambu ABS @base in both slicers. Given the same treatment as colourOf, reusing firstScalar so an empty string or non-string yields null rather than a fabricated value. Note it has only one spelling; unlike colour there is no default_filament_vendor in either tree.

3. Degrade paths now log

catch { fields = leaf; } and the outer catch { continue; } both logged nothing, which is why bambuddy#47 and AFKFelix#51 each took a ticket to localize. Both now console.warn.

Added beyond the ticket: a console.info resolved-count line once per cache fill. The commonest silent failure — a dangling inherits — by design neither throws nor warns, so only the counts make a tier-wide regression to all-null visible.

Measured against the real upstream trees

The endpoint run against the actual resources/profiles/BBL trees at the pinned versions (still no image — these are the upstream repos):

filament typed before typed after vendor after cold listing
OrcaSlicer 2.3.2 974 920 (94.5%) 942 (96.7%) 942 325 ms -> 434 ms
BambuStudio 02.07.01.57 1618 1584 (97.9%) 1618 (100%) 1618 466 ms -> 635 ms

Slash-named presets: 22 (Orca) and 34 (BambuStudio), 0 typed before, all typed after. Bambu Support For PA/PET @BBL A1 now resolves PA / Bambu Lab.

This also independently confirms #2's headline numbers (920/974 and 1584/1618) and its cold-listing figures (300/471 ms), which had been measured but not reproduced outside the images.

Cold-listing cost, stated honestly: +109 ms Orca, +169 ms BambuStudio. That is the one directory enumeration the index build performs, paid once per cache fill (1 h TTL) and only because slash-named presets exist at all. The per-preset hot path is unchanged.

Correcting the record from #2

#2's description, and its comment on bambuddy#51, state that the Support For PA/PET and PLA/PETG families' @base parent is "genuinely absent from the shipped tree" and their material "not knowable from the bundled JSON". That is false. The parents ship in both slicers under sanitized filenames; the walk simply could not reach them:

Bambu Support For PA/PET @BBL X1C -> Bambu Support For PA/PET @base -> fdm_filament_pa    -> ["PA"]
Bambu Support For PLA/PETG @BBL A1 -> Bambu Support For PLA/PETG @base -> fdm_filament_pla -> ["PLA"]

Identical in both slicers.

The 32 still untyped on OrcaSlicer are a different bug

They are eSUN PLA+ @base, Overture PLA @base, PolyLite PETG @base and friends — parents that live in BBL/filament/ subdirectories, which readBundledDir never reads because its readdir is non-recursive (9 subdirectories, 615 files on Orca; 3 and 150 on BambuStudio). Deliberately out of scope here and filed separately. BambuStudio reaches 100% because its untyped set was entirely the slash families.

Tests

+20 tests (8 unit, 12 e2e). tests/e2e/bundled-filament-metadata.spec.ts from #2 keeps passing untouched.

Tests
base bambuddy/profile-resolver 100 passed, 22 failed (122)
this branch 120 passed, 22 failed (142)

The 22 failures are pre-existing and environmental — identical set on both branches, verified by diffing failing test names. They are the slice/schema e2e specs, which need BUNDLED_PROFILES_PATH/ORCASLICER_PATH and the slicer binary that only exists inside the image. Zero regressions.

tsc --noEmit clean. eslint reports 3 errors, all pre-existing and in files this PR does not touch (health.service.ts:48, ecosystem.config.cjs:1, bundle.service.spec.ts:138); zero new.

Mutation-checked

Each fix was made inert and the corresponding tests confirmed to fail — run independently of the implementation:

mutation result
revert parent lookup to plain path.join 7 fail — both slash tests (unit + e2e), the .. traversal test (expected 'yes' to be undefined — the traversal really did leak), the index-caching test, the count line
filament_vendor forced to null 4 fail — incl. emits filament_vendor resolved from an ancestor
both console.warn + console.info silenced 3 fail — exactly the three logging tests
restored 0 fail

One honest non-result: the absolute inherits test passes under the reverted lookup too — path.join(dir, "/abs/path") happens to produce a nonexistent nested path, so the old code failed to exfiltrate by luck rather than design. It is a regression guard, not a mutation-proven one. The .. test is the one that actually catches traversal.

Not verified here

Nothing in this PR has been run inside a rebuilt image — no Docker or registry command was run in this session, deliberately. Still requiring the rebuild session: behaviour against the images' own trees (which may differ from upstream at these tags), real cold-listing latency in-container, and bambuddy#51's end-to-end criterion that a stock install auto-picks an ABS profile for an ABS plate.

Bambuddy-side, filament_vendor is already read at backend/app/api/routes/slicer_presets.py:447, so it should flow through with no Bambuddy change — worth confirming, and it likely obsoletes bambuddy#62 (the Bambu -> Bambu Lab alias), which should not be closed until checked against a rebuilt image.

…r, log degrades

Three related silent failures in the bundled-profile inheritance walk.

1. Slash-named parents broke the walk. Some bundled profiles declare a
   `name` containing a literal `/` while the file on disk substitutes
   something else — and the substitute is not consistent:

     "Bambu Support For PA/PET @base"   -> "Bambu Support For PA PET @base.json"
     "Bambu Support For PLA/PETG @base" -> "Bambu Support For PLA-PETG @base.json"

   profile-resolver derived the parent's path from the `inherits` string,
   so the `/` acted as a directory separator, the read ENOENTed, and the
   whole remaining ancestor chain was dropped without a word. Because the
   walk is shared with the slice path and /resolved-process, a user preset
   inheriting such a base sliced under-specified, silently.

   Parents are now located by each bundled file's *declared* `name` via a
   directory index rather than by re-deriving a path — the sanitization
   scheme is undocumented and demonstrably differs per case, so guessing at
   it is the wrong approach. The direct path is still tried first and the
   index is built lazily only on a miss, memoised per category directory:
   resolveProfile runs once per preset in a ~2500-file listing loop and the
   basename matches for the overwhelming majority, so the hot path stays a
   single readFile and cold-listing latency is unchanged.

   This also closes a path traversal: a `..` (or absolute path) in an
   `inherits` value previously reached outside the profiles directory by
   the same mechanism. The direct lookup now refuses anything that is not a
   plain basename, and the index can only ever name files enumerated from
   inside the category directory.

2. `filament_vendor` sits on the family base ("Bambu ABS @base" ->
   ["Bambu Lab"]), one hop up the chain the walk already traverses, and was
   never read. It now gets the same treatment as colour, reusing firstScalar
   so an empty string or non-string yields null rather than a fabricated
   value. It has only one spelling — no second key was invented.

3. Both degrade paths in GET /profiles/bundled logged nothing, so a
   systematic resolution failure would present as the endpoint quietly
   answering all-null again. Both now warn, and the listing emits a
   resolved-count line — which also covers the dangling-parent case, whose
   degrade is silent by design and never throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jappyjan
jappyjan merged commit 6e5d665 into bambuddy/profile-resolver Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant