feat: three-layer codec killbits (draft, do not merge) - #720
Draft
lilith wants to merge 6 commits into
Draft
Conversation
lilith
added a commit
that referenced
this pull request
Apr 20, 2026
…text.security Previously `Build001`/`Execute001` inline `security` blocks mutated `Context.security` in place, leaking per-job intent across subsequent jobs on the same Context. That persisted even after #720's intersection-only change — the mutation was still there, just intersected before being applied. Fix the root cause: * Split `Context.security` into `default_job_security` (Context-scoped, set only by `v1/context/set_policy`) and `active_job_security` (per-job, scoped to the lifetime of a single build/execute). * Add pure `Context::effective_security(&self, inline)` returning `trusted_policy ∩ default_job_security ∩ inline` — no mutation. * `build_inner`/`execute_inner` validate the inline block, compute the effective value, install it into `active_job_security` via `JobSecuritySnapshot::install`, run the job, then restore. * `Context::current_security()` is the single read point for per-node limit checks (`max_decode_size`, `max_frame_size`, `max_encode_size`, `max_input_file_bytes`, `max_total_file_pixels`) — returns the active value when mid-job, the default otherwise. * `net_support()` prefers the already-intersected `active_job_security` over any job_request parameter so every dispatch point reads the same grid. * `configure_security` is removed; its policy-install behaviour lives on as `apply_trusted_policy`, reachable only from `set_trusted_policy`. `max_json_bytes` is still read pre-parse from `default_job_security` (the parse has to know the cap before it can see the job's own security block). That's the one field that stays Context-scoped. The existing three-layer killbits tests continue to pass unchanged.
4 tasks
lilith
force-pushed
the
feat/killbits-three-layer
branch
from
April 21, 2026 00:28
a6a1b62 to
d32b52b
Compare
lilith
added a commit
to imazen/imageflow-dotnet
that referenced
this pull request
Apr 21, 2026
Widen Imageflow.NativeRuntime.* / Imageflow.NativeTool.* version ranges from the implicit "2.3.1-rc01 or higher" to "[2.3.1-rc01, 4.0.0)" so restores will pick up a v3.x native runtime once imazen/imageflow#720 ships a killbits-capable prerelease, without bumping csprojs again. Range choice: - Floor remains 2.3.1-rc01 — NuGet's lowest-applicable rule keeps today's resolution unchanged on existing v2-only feeds. - Prerelease lower bound is required: no stable 2.x exists, and a stable lower bound ([2.0.0, 4.0.0)) would exclude all prereleases and fail to restore. - Upper cap <4.0.0 permits v3 adoption while preventing unintended jumps across a future v4 major. Scope: - src/Imageflow.AllPlatforms/Imageflow.AllPlatforms.csproj Imageflow.NativeRuntime.All: "2.3.1-rc01" -> "[2.3.1-rc01, 4.0.0)" - tests/Imageflow.Test/Imageflow.Test.csproj All 7 Imageflow.NativeTool.<rid> entries: same widening. - Regenerated packages.lock.json for both projects; resolved versions remain 2.3.1-rc01 (verified via lowest-applicable). Out of scope (deliberate): - tests/Imageflow.TestDotNetFull/*.csproj + packages.config stay pinned to 2.3.1-rc01. packages.config-style restore embeds the package version into the .targets import path (src/packages/Imageflow.NativeRuntime.win-x86_64.2.3.1-rc01/ build/net45/...); ranges are not supported in that format. v3 may also change the targets file layout. This project is legacy net462/net472 gated behind the test_legacy matrix on Windows. - Client code is untouched. SetPolicy / GetNetSupport / killbits endpoints ride on the existing imageflow_context_send_json ABI; calls against a v2 native correctly return InvalidMessageEndpoint, and KillbitsIntegration tests are already gated behind IMAGEFLOW_HAS_KILLBITS=1 (unchanged). Verified: - dotnet restore ./src/Imageflow.dncore.sln --force-evaluate: OK - dotnet build ./src/Imageflow.dncore.sln -c Release: 0 errors across net8.0 / net9.0 / net10.0 / netstandard2.0 / netstandard2.1. - dotnet test --framework net10.0 --filter "Category!=KillbitsIntegration": 150 passing (matches PR #68 baseline). - dotnet test --framework net8.0 --filter same: 150 passing. Follow-up when a v3 killbits-capable prerelease publishes: - Bump Imageflow.Test's floor to [3.0.0-alpha.0, 4.0.0) to force v3 selection and exercise KillbitsIntegration under IMAGEFLOW_HAS_KILLBITS=1. - Imageflow.AllPlatforms can keep [2.3.1-rc01, 4.0.0) so downstream consumers still build against v2 by default until v3 is ready.
5 tasks
lilith
force-pushed
the
feat/killbits-three-layer
branch
2 times, most recently
from
April 21, 2026 05:24
78c0f45 to
3fe6a96
Compare
…ncoder,Decoder}Name, build_killbits consts, ExecutionSecurity extensions
Introduce the three-layer killbits system at the types level.
- `ImageFormat` enum — the canonical list of codec-eligible formats.
- `FormatKillbits` / `FormatPermissions` / `FormatGrid` — per-format
decode/encode gating with intersection semantics.
- `CodecKillbits` + `NamedEncoderName` / `NamedDecoderName` — finer-
grained per-codec gating (e.g. allow zen_jpeg_encoder but not mozjpeg)
layered on top of the format-level grid.
- `build_killbits` — compile-time `COMPILE_DENY_DECODE` /
`COMPILE_DENY_ENCODE` const arrays populated from feature-gate
enablement, used later by the core to compute the compile ceiling.
- `ExecutionSecurity::{formats, codecs}` — new optional boxed fields
carrying the per-format / per-codec killbits. Boxed to keep the
struct small (~120 bytes of inline list capacity stays on the heap).
- `SetPolicyRequest` wire type for `v1/context/set_policy`.
- Validation helpers (`KillbitsValidationError`, `CodecKillbitsJobLevelError`,
etc.) surface mutual-exclusion errors and job-level "deny-only"
violations as structured error values.
Pure types crate; no core wiring yet.
Previously `Build001`/`Execute001` inline `security` blocks mutated `Context.security` in place, leaking per-job intent across subsequent jobs on the same Context. Fix the root cause: * Rename `Context.security` to `default_job_security` (Context-scoped defaults). Never mutated by job-level `security` JSON — job-level intent lives in `active_job_security` for the lifetime of the job. * Add `Context.active_job_security: Option<Box<ExecutionSecurity>>` — per-job effective value, set for the duration of a single `build`/`execute` call and cleared when the job finishes. Boxed to keep the idle-Context footprint small. * Add pure `Context::effective_security(&self, inline)` returning `default_job_security ∩ inline` — no mutation. * Add `JobSecuritySnapshot::install` / `restore` — scope guard that swaps `active_job_security` for the job body and restores afterward. * `build_inner` / `execute_inner` validate the inline block, compute the effective value, install it via `JobSecuritySnapshot`, run the job, then restore. * `Context::current_security()` is the single read point for per-node limit checks (`max_decode_size`, `max_frame_size`, `max_encode_size`, `max_input_file_bytes`, `max_total_file_pixels`) — returns the active value when mid-job, the default otherwise. * Codec decoders (gif, png, webp, zen) and encode-path limit checks (`execution_engine`, `codecs_and_pointer`) route through `current_security()` instead of reading `ctx.security` directly. * `configure_security` is removed; the per-job mutation path is gone. `max_json_bytes` is still read pre-parse from `default_job_security` (the parse has to know the cap before it can see the job's own security block). That's the one field that stays Context-scoped. The trusted-policy layer (layer 2) is introduced in a follow-up commit; this commit strictly fixes the job-scoping bug and exposes the hooks the trusted layer will plug into.
…endpoints
Layer 2 of the three-layer killbits system: a Context-scoped trusted
policy set once via a new JSON endpoint, intersected with the
per-job effective security on every build/execute.
Context:
- `Context.trusted_policy: Option<Box<ExecutionSecurity>>` — set at
most once per Context (from `v1/context/set_policy`). Boxed to
keep the idle-Context footprint small.
- `Context::set_trusted_policy(policy, require_unlocked)` — installs
the policy. Narrow-only on re-lock; rejects `allow_*` entries
naming formats denied at build time.
- `Context::effective_security` now intersects `trusted_policy ∩
default_job_security ∩ inline` (layer 1 ∩ layer 2 ∩ layer 3).
- `Context::net_support` / `Context::codec_support` — return the
effective format and codec grids.
New `imageflow_core::killbits` module:
- `feature_compiled_in(format, op)` — runtime feature-flag map for
the c-codecs / zen-codecs gates.
- `parse_format_name` / `from_output_format` — string/enum to
`ImageFormat`.
- `compute_net_support` / `compute_net_support_with_codecs` /
`build_ceiling_grid` — grid math across layers 1..3.
- `enforce(grid, op, format)` — produces structured
`decode_not_available` / `encode_not_available` errors when denied.
- `intersect_security` / `ensure_narrowing` / `validate_trusted_*`
helpers for policy installation and per-job merging.
- `NetSupport` / `CompileCeiling` / `LockedPolicyReport` /
`CodecSupportGrid` / `FormatGridView` wire types.
`NamedDecoders::wire_name` / `image_format` and
`NamedEncoders::wire_name` expose the symmetric mappings the killbits
grid uses to enumerate live backends.
Endpoints (via the existing `imageflow_context_send_json` entry
point; no new C ABI):
- `v1/context/set_policy` — request body
`{ policy: ExecutionSecurity, require_unlocked?: bool }`. Sets the
trusted policy once; later calls are accepted only when narrowing.
Response echoes the resulting `net_support` grid.
- `v1/context/get_net_support` — no request body. Returns the current
grid, whether a trusted policy is set, and a `compile_ceiling`
summary (denied decode, denied encode, and formats with no
compiled-in backend).
The OpenAPI schema regeneration lands in a follow-up commit.
Gate every live decode/encode dispatch on the net_support grid (layer 1 ∩ layer 2 ∩ layer 3) and the codec-level kill lists. Decode dispatch (`EnabledCodecs::create_decoder_for_magic_bytes`): - Compute the effective grid once per request; call `killbits::enforce(grid, Op::Decode, format)` for each matching decoder. If the format is denied, return a structured `decode_not_available` error. - Codec-level kill: when a matching decoder's wire name is denied by trusted/active codec killbits, skip it and try the next matching decoder (mozjpeg killed → fall through to image-rs JPEG). Only error `codec_not_available` when every matching decoder is dead. - Distinguish "no decoder handled magic bytes" from "every matching decoder was killed" so operators can diagnose configuration vs. unsupported-input errors. Encode dispatch (`codecs/auto.rs`): - `auto::create_encoder` and `EncoderPreset` parsing call `killbits::enforce(grid, Op::Encode, format)` with the output format derived from the preset. Denied formats produce `encode_not_available` errors before any encoder is instantiated. - When a format's preferred encoder is killed, fall back to the highest-priority live encoder for that format; return `codec_not_available` only when all encoders for the format are denied. Every denial path carries a structured JSON payload (`error`, `format`, `reasons`) that the ABI layer passes through unchanged.
Broad test surface for the three-layer killbits system: Types-level unit tests (in `imageflow_types::killbits`): killbits `validate`, `validate_job_level`, `intersect`, `apply_to` coverage for both `FormatKillbits` and `CodecKillbits`. Verifies mutual exclusion (allow_* xor deny_* xor formats table), job-level "deny-only" rejection, and that intersection is the union of the two layers' denies. Core-level unit tests (in `imageflow_core::killbits`): `build_ceiling_grid` reflects feature gates, `intersect_security` narrows scalar limits and combines killbits, `compute_net_support` folds the three layers, `codec_not_available_error` surfaces structured JSON. Integration tests (`imageflow_core/tests/integration/killbits.rs`): - End-to-end: `Context::set_trusted_policy` rejects `allow_decode` naming format denied at build time; accepts narrowing re-locks; errors on widening re-locks. - Dispatch: `create_decoder_for_magic_bytes` returns `decode_not_available` when the format is killed, and `codec_not_available` when every matching decoder is killed. - Encode: `create_encoder` returns `encode_not_available` / falls back to a live encoder when the preferred one is killed. - Scoping regression (`inline_max_decode_size_is_not_persisted_across_jobs`, `inline_killbits_is_not_persisted_across_jobs`, `trusted_policy_persists_but_inline_does_not`): confirms that `default_job_security` is untouched by inline job security, and that trusted-policy-installed scalar limits persist while inline denies do not. - Cache: `net_support_cache_hands_out_same_arc_when_policy_unchanged`, `net_support_cache_is_invalidated_by_set_policy`, `net_support_per_job_inline_security_bypasses_cache`, `compile_ceiling_is_process_static` pin the caching contract.
Pick up the new `v1/context/set_policy` and `v1/context/get_net_support` endpoints plus the `ExecutionSecurity.formats` / `ExecutionSecurity.codecs` fields and the `ImageFormat` / `FormatPermissions` / `FormatKillbits` / `CodecKillbits` / `NamedEncoderName` / `NamedDecoderName` schemas in `openapi_schema_v1.json`. Updated hash in `openapi_schema_v1.json.hash` so the `hash_files_relevant_to_schema_and_compare` drift test passes. Generated by `cargo test -p imageflow_core --features schema-export,json-schema` (the test rewrites both files in place).
lilith
force-pushed
the
feat/killbits-three-layer
branch
from
April 21, 2026 05:55
9c7c243 to
8e435a1
Compare
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Three things to land in this PR:
Stop leaking per-job security intent across jobs on the same
Context.Today
Build001/Execute001inlinesecurityblocks mutateContext.securityin place, so one job's narrowing persists andaffects the next job on that Context. An earlier iteration of this
PR made the mutation intersection-only when a
trusted_policyisset, but the mutation itself was still there. This version fixes it
properly: inline job security is scoped to the lifetime of one
build/executecall and never writes through to Context state.Add a three-layer killbits system (format + codec) so trusted
applications can pin the codec surface once and treat every
downstream request as a strict narrower. Zero users of killbits
today, so breaking in-flight shapes is free.
Graceful codec substitution with observability. When a
specific-codec
EncoderPresetnames a codec that's unavailable butanother codec for the same wire format is live, the dispatcher
substitutes the available codec and surfaces a machine-readable
annotation on the response rather than failing the request.
The three layers
imageflow_types::build_killbits::COMPILE_DENY_{DECODE,ENCODE}+feature_compiled_inv1/context/set_policy→Context::trusted_policysecurityfield onBuild001/Execute001, extended withformatsandcodecsallow_*+ table entries withdecode: true/encode: trueare rejected.The effective grid for any operation is
layer 1 ∩ layer 2 ∩ layer 3.Enforcement fires at decoder instantiation (when the input buffer is
added) and at encoder dispatch (inside
auto::create_encoder),producing a structured JSON error:
{ "error": "decode_not_available", "format": "jxl", "reasons": ["compile.feature_missing"], "net_support": { "formats": { ... }, "codecs": { ... } } }Per-job scoping (Part A — the pre-existing bug)
Context.security(mutable, process-wide) is replaced by threeContext fields that make the scoping explicit:
default_job_security: ExecutionSecurity— Context-scoped defaults,initialized from
sane_defaults()or the trusted policy. Read-onlyduring job execution.
trusted_policy: Option<Box<ExecutionSecurity>>— the layer-2baseline, set by
v1/context/set_policy. Never mutated by job JSON.active_job_security: Option<Box<ExecutionSecurity>>— the per-jobeffective value (
trusted ∩ default ∩ inline), installed for theduration of a single
build/executeand torn down on exit.Context::effective_security(&self, inline)is the pure computation;Context::current_security()is the single read point for per-nodelimit checks. Inline
securityonBuild001/Execute001no longerpersists to
Context.default_job_security.max_json_bytesis the one field that stays Context-scoped — itbounds the JSON parse itself, so it has to be known before the job's
own
securityblock is visible.Three regression tests pin this:
inline_max_decode_size_is_not_persisted_across_jobsinline_killbits_is_not_persisted_across_jobstrusted_policy_persists_but_inline_does_notCodec-level killbits (Part B)
Format-level killbits grant/deny every codec for a given format.
Codec killbits add per-backend granularity: an operator can forbid
mozjpeg_encoderwhile keepingzen_jpeg_encoder, or disable aspecific decoder that's had a CVE without stubbing the format.
Types
In
imageflow_types:NamedEncoderName/NamedDecoderName—#[non_exhaustive]snake_case serde enums mirroring every known
NamedEncoders/NamedDecodersvariant in core. Purely nominal; feature gating ishandled at the core layer.
CodecKillbits { allow_encoders, deny_encoders, allow_decoders, deny_decoders }with the same validation (mutual exclusion,validate_job_levelrejectingallow_*) asFormatKillbits.ExecutionSecurity.codecs: Option<Box<CodecKillbits>>.JSON examples
Allow-list form (trusted-policy only):
{ "policy": { "codecs": { "allow_encoders": ["mozjpeg_encoder", "lodepng_encoder", "zen_webp_encoder"], "allow_decoders": ["mozjpeg_rs_decoder", "libpng_decoder", "gif_rs_decoder"] } } }Deny-list form (trusted-policy or job-level):
{ "policy": { "codecs": { "deny_encoders": ["pngquant_encoder"], "deny_decoders": ["zen_jxl_decoder"] } } }Job-level narrowing:
{ "framewise": { "steps": [ ... ] }, "security": { "codecs": { "deny_encoders": ["mozjpeg_encoder"] } } }Net support grid (format × codec interaction)
v1/context/get_net_supportresponse grows acodecssubtree andper-format
{decode,encode}_reasonsvectors:{ "ok": true, "net_support": { "formats": { "jpeg": { "decode": true, "encode": true }, "png": { "decode": true, "encode": true }, "avif": { "decode": true, "encode": false, "encode_reasons": ["no_available_encoder"] } }, "codecs": { "mozjpeg_encoder": { "available": true, "format": "jpeg", "role": "encode" }, "zen_jpeg_encoder": { "available": true, "format": "jpeg", "role": "encode" }, "zen_avif_encoder": { "available": false, "format": "avif", "role": "encode", "reasons": ["format_denied"] }, "libpng_encoder": { "available": false, "format": "png", "role": "encode", "reasons": ["codec_killbits.deny_encoders"] } } }, "trusted_policy_set": true, "compile_ceiling": { "denied_decode": [], "denied_encode": [], "features_missing": ["heic", "tiff", "pnm"] } }Graceful codec substitution (Part C — new in this revision)
Wire format vs implementation
The previous iteration of this PR treated each specific-codec
EncoderPresetvariant as a contract to use that exact codec, andrejected the request when the codec was denied. That's too strict for
the common operator case: "deny the C mozjpeg build because of a CVE,
but keep JPEG encoding working via zenjpeg." Callers that only care
about getting a valid JPEG back (the overwhelming majority) end up
with 422s they can't act on.
This revision splits the contract into two tiers:
honored: a request for JPEG always produces JPEG bytes, never PNG
bytes.
EncoderPreset::Format { format: avif }with AVIF deniedstill errors with
encode_not_available— the format contract issacred.
preference. When the requested codec is unavailable but another
codec for the same wire format is live, the dispatcher substitutes
and annotates the response.
Substitution table
Mozjpeg { quality, progressive, matte }ZenJpegEncoder→MozjpegRsEncoderLibjpegTurbo { quality, progressive, optimize_huffman_coding, matte }ZenJpegEncoder(only)MozjpegRsEncoder— always optimizes Huffman, can't honor the disable toggleLibpng { depth, matte, zlib_compression }ZenPngEncoder→LodepngEncoderzlib_compressiondropped on lode/zen path (annotated)Lodepng { maximum_deflate }ZenPngEncoder→LibpngEncodermaximum_deflate=truetranslated to zlib=9 on libpngPngquant { ... }format_not_availableWebPLossy { quality }WebPLossless {}Annotation shape
Each encoded image in the response carries an optional
annotationsbag. Today's only channel is
codec_substitution:{ "encodes": [{ "io_id": 1, "preferred_extension": "jpg", "preferred_mime_type": "image/jpeg", "w": 800, "h": 600, "bytes": ..., "annotations": { "codec_substitution": { "requested": "mozjpeg_encoder", "actual": "zen_jpeg_encoder", "reason": "codec_killbits_deny_encoders", "field_translations": [ "preset.quality → zen.quality", "preset.progressive → zen.progressive" ], "dropped_fields": [] } } }] }reasonvalues (serializedsnake_case):codec_killbits_deny_encoders— requested codec was in a job ortrusted
deny_encoderslist.codec_killbits_allow_encoders_excludes— requested codec wasn'tin an
allow_encoderslist.compile_feature_missing— the build didn't include the requestedcodec's feature gate.
compile_codec_const_denied— the format family is inCOMPILE_DENY_ENCODE.not_registered— the codec isn't in the runtimeenabled_codecsregistry.The annotation is per-encode-step (attached to
EncodeResult), notper-job. A response with multiple encodes can have different
substitutions per step.
EncodeAnnotationsis forward-extensible —additional annotation kinds can be added as sibling fields without
breaking callers that only understand
codec_substitution.Error shape when substitution is impossible
When the format itself has no live encoder (
net.encode(format) = false) after all killbits + features are applied, the unifiedformat_not_availableerror fires at parse time:{ "error": "format_not_available", "format": "jpeg", "requested_codec": "mozjpeg_encoder", "reasons": ["all_jpeg_encoders_denied"], "net_support": { ... } }This also fires when the requested codec has no substitution
candidates — today just
Pngquant(palette quantization is unique).The pre-existing
encode_not_availableformat-level error(
EncoderPreset::Format { format: jpeg }when JPEG is off) anddecode_not_availabledecoder-side error are unchanged.Backward compatibility
Every existing workflow that uses a legacy
EncoderPresetvariant(
Mozjpeg,LibjpegTurbo,Libpng,Lodepng,WebPLossy,WebPLossless) continues to work even when operators tighten codeckillbits — the dispatcher finds a substitute for the same wire format
and annotates the response. Clients that don't read annotations are
unaffected.
code:codec_not_availableis still a valid error type on the schemafor the rare unsubstitutable case (e.g., Pngquant denied with no
quantizer substitute). Consumers that already match on it keep
working.
Decoder side
Decoder substitution follows the same pattern the encoder dispatch
landed in commit 4:
create_decoder_for_magic_byteswalks allmatching decoders, skips any that are codec-killed, and only errors
when every decoder for the format is down. That logic pre-dates this
revision and already satisfies the substitution contract from the
decode side. Surfacing a structured per-decode annotation is tracked
as follow-up work —
DecodeResult.annotationsandDecoderSubstitutionAnnotationare defined on the types side so theschema is ready, but the core-side plumbing to populate them is
scoped to a follow-up PR.
Breaking behavior changes (would break hypothetical callers; today there are none)
securityonBuild001/Execute001no longer persistsinto
Context.security. Callers that relied on "first job setsthe limit, subsequent jobs inherit it" (not a documented contract,
but possible in practice) must instead install their baseline via
v1/context/set_policy.EncoderPresetvariants now substitute when therequested codec is denied and a same-format codec is available,
instead of silently substituting on an outdated table. The request
succeeds; the response carries a
codec_substitutionannotation.When no substitute exists (
Pngquantdenied, or every same-formatcodec denied), the request errors with
format_not_available.Context::configure_securityis removed; its policy-set semanticslive on as a private
apply_trusted_policyreachable only throughContext::set_trusted_policy.test_context_size,test_thread_safe_context_size,test_calculate_context_heap_size) adjusted for the two newOption<Box<ExecutionSecurity>>fields, the newOption<Box<CodecKillbits>>onExecutionSecurity, and theOption<Box<EncodeAnnotations>>on eachCodecInstanceContainer.Non-breaking changes
ExecutionSecurityis#[non_exhaustive], so the newcodecsfield is additive.
NamedEncoderName/NamedDecoderNameare#[non_exhaustive].EncodeAnnotationsis forward-extensible (all fields optional).SubstitutionReasonis#[non_exhaustive].security.formatsorsecurity.codecsbehaveidentically to before.
imageflow_abi/src/lib.rshas no new exports andno signature changes. New endpoints are reached through the
existing
imageflow_context_send_jsonentry point.What this PR does NOT do
imageflow_abi/src/lib.rschanges.imageflow_riapichanges.accept.*URL translation.ProcessConfig-style startup-mutable type.DecodeResult(types are defined, core plumbing is a follow-up).
Test plan
cargo check --workspaceis green at every commit.cargo test -p imageflow_types --lib— 38 pass, 0 fail(added
CodecKillbits-specific tests,SubstitutionReasonserde tests,
CodecSubstitutionAnnotationround-trip tests,EncodeAnnotationsshape tests).cargo test -p imageflow_core --lib— 78 pass, 0 fail.cargo test -p imageflow_core --test integration killbits—27 pass, 0 fail. Substitution coverage:
deny_specific_encoder_substitutes_preset_when_lodepng_denied,lodepng_preset_errors_format_not_available_when_no_png_encoder_remains,pngquant_preset_errors_format_not_available_when_pngquant_denied,mozjpeg_preset_substitutes_when_mozjpeg_denied,libjpegturbo_preset_substitutes_mozjpeg_denied_to_zenjpeg_not_mozrs,libpng_preset_substitutes_when_libpng_denied,lodepng_preset_substitutes_when_lodepng_denied,webp_lossy_preset_substitutes_when_primary_webp_denied,webp_lossless_preset_substitutes_when_primary_webp_denied,format_preset_denied_stays_strict_no_substitution.cargo test -p imageflow_core --test integration -- --skip visuals— 99 pass, 0 fail, 3 ignored (pre-existing).
cargo test -p imageflow_core --features schema-export,json-schema hash_files_relevant_to_schema_and_compare— pass, schema + hashregenerated and committed.
Intel, macOS ARM, Windows, windows-11-arm) — pending.
Commits
feat(types): ImageFormat enum, FormatKillbits, CodecKillbits, Named{Encoder,Decoder}Name, build_killbits consts, ExecutionSecurity extensions, EncodeAnnotations / CodecSubstitutionAnnotation / SubstitutionReason, EncodeResult.annotationsrefactor(core): scope per-job security; stop mutating Context.securityfeat(core): trusted_policy + v1/context/{set_policy,get_net_support} endpointsfeat(core): enforce format + codec killbits at decode/encode dispatch; substitute same-format codec for denied preset + annotate; format_not_available when no substitute availabletest: killbits + substitution + scoping unit + integration coverage (types + core)chore: regenerate OpenAPI schema hashDraft — not for merge. Opened to invite review of the surface /
enforcement points / error + annotation shape before widening.