fix: harden path lookup and mark gl backend gap

This commit is contained in:
2026-06-22 16:12:57 +04:00
parent 5436727961
commit f69c893a40
7 changed files with 209 additions and 47 deletions
+17 -17
View File
@@ -1,5 +1,5 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! SDL platform adapter proof behind safe `FParkan` ports. //! SDL platform adapter boundary stubs behind safe `FParkan` ports.
use fparkan_platform::{ use fparkan_platform::{
EventSource, GraphicsContextRequest, GraphicsProfile, PhysicalSize, PlatformError, EventSource, GraphicsContextRequest, GraphicsProfile, PhysicalSize, PlatformError,
@@ -33,20 +33,20 @@ impl Default for SdlAdapterCapabilities {
} }
} }
/// Returns adapter readiness status for the safe project-owned layer. /// Returns whether the project-owned adapter boundary avoids `unsafe`.
#[must_use] #[must_use]
pub fn safe_adapter_ready() -> bool { pub fn project_owned_layer_unsafe_free() -> bool {
SdlAdapterCapabilities::default().project_owned_unsafe_free SdlAdapterCapabilities::default().project_owned_unsafe_free
} }
/// In-memory event source used by adapter smoke tests and composition roots /// In-memory event source used by adapter smoke tests before a concrete SDL
/// before a concrete SDL runtime is injected. /// runtime is selected.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SdlEventSourceProof { pub struct SdlEventSourceStub {
pending: Vec<PlatformEvent>, pending: Vec<PlatformEvent>,
} }
impl SdlEventSourceProof { impl SdlEventSourceStub {
/// Creates an event source with deterministic pending events. /// Creates an event source with deterministic pending events.
#[must_use] #[must_use]
pub fn new(pending: Vec<PlatformEvent>) -> Self { pub fn new(pending: Vec<PlatformEvent>) -> Self {
@@ -54,22 +54,22 @@ impl SdlEventSourceProof {
} }
} }
impl EventSource for SdlEventSourceProof { impl EventSource for SdlEventSourceStub {
fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> { fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> {
out.append(&mut self.pending); out.append(&mut self.pending);
Ok(()) Ok(())
} }
} }
/// Safe window-port proof with SDL-compatible drawable-size semantics. /// Safe window-port stub with SDL-compatible drawable-size semantics.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct SdlWindowProof { pub struct SdlWindowStub {
size: PhysicalSize, size: PhysicalSize,
presents: u64, presents: u64,
} }
impl SdlWindowProof { impl SdlWindowStub {
/// Creates a proof window with a fixed drawable size. /// Creates a stub window with a fixed drawable size.
#[must_use] #[must_use]
pub fn new(size: PhysicalSize) -> Self { pub fn new(size: PhysicalSize) -> Self {
Self { size, presents: 0 } Self { size, presents: 0 }
@@ -82,7 +82,7 @@ impl SdlWindowProof {
} }
} }
impl WindowPort for SdlWindowProof { impl WindowPort for SdlWindowStub {
fn drawable_size(&self) -> PhysicalSize { fn drawable_size(&self) -> PhysicalSize {
self.size self.size
} }
@@ -98,20 +98,20 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn adapter_reports_safe_project_layer_ready() { fn adapter_boundary_is_project_owned_unsafe_free() {
assert!(safe_adapter_ready()); assert!(project_owned_layer_unsafe_free());
assert_eq!(SdlAdapterCapabilities::default().graphics.len(), 2); assert_eq!(SdlAdapterCapabilities::default().graphics.len(), 2);
} }
#[test] #[test]
fn event_source_and_window_ports_are_deterministic() -> Result<(), PlatformError> { fn event_source_and_window_ports_are_deterministic() -> Result<(), PlatformError> {
let mut source = SdlEventSourceProof::new(vec![PlatformEvent::Quit]); let mut source = SdlEventSourceStub::new(vec![PlatformEvent::Quit]);
let mut events = Vec::new(); let mut events = Vec::new();
source.poll(&mut events)?; source.poll(&mut events)?;
source.poll(&mut events)?; source.poll(&mut events)?;
assert_eq!(events, vec![PlatformEvent::Quit]); assert_eq!(events, vec![PlatformEvent::Quit]);
let mut window = SdlWindowProof::new(PhysicalSize { let mut window = SdlWindowStub::new(PhysicalSize {
width: 320, width: 320,
height: 240, height: 240,
}); });
+6 -6
View File
@@ -1,5 +1,5 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! OpenGL render adapter proof behind safe `FParkan` render ports. //! OpenGL render adapter boundary stubs behind safe `FParkan` render ports.
use fparkan_render::{ use fparkan_render::{
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
@@ -64,9 +64,9 @@ impl Default for GlAdapterCapabilities {
} }
} }
/// Returns adapter readiness status for the safe project-owned layer. /// Returns whether the project-owned adapter boundary avoids `unsafe`.
#[must_use] #[must_use]
pub fn safe_adapter_ready() -> bool { pub fn project_owned_layer_unsafe_free() -> bool {
GlAdapterCapabilities::default().project_owned_unsafe_free GlAdapterCapabilities::default().project_owned_unsafe_free
} }
@@ -98,7 +98,7 @@ pub fn compile_shader_source(
Ok(()) Ok(())
} }
/// Safe render backend facade used for adapter-level command validation. /// Safe render backend stub used for adapter-level command validation.
/// ///
/// A concrete OpenGL implementation can be injected behind the same /// A concrete OpenGL implementation can be injected behind the same
/// [`RenderBackend`] port once an audited safe GL facade is selected. This type /// [`RenderBackend`] port once an audited safe GL facade is selected. This type
@@ -147,8 +147,8 @@ mod tests {
}; };
#[test] #[test]
fn adapter_reports_safe_project_layer_ready() { fn adapter_boundary_is_project_owned_unsafe_free() {
assert!(safe_adapter_ready()); assert!(project_owned_layer_unsafe_free());
assert_eq!(GlAdapterCapabilities::default().profiles.len(), 2); assert_eq!(GlAdapterCapabilities::default().profiles.len(), 2);
} }
+22 -1
View File
@@ -2,4 +2,25 @@
Status: provisional Status: provisional
Workspace-owned code forbids `unsafe`. SDL/OpenGL adapters must use maintained external crates behind a safe project API. The current repository still contains legacy demo code scheduled for replacement; new adapter crates are placeholders until a fully audited safe facade is selected and proven on all target profiles. Workspace-owned code forbids `unsafe`. SDL/OpenGL adapters must use maintained
external crates behind a safe project API; local Objective-C/CGL/SDL/OpenGL FFI
inside FParkan is not an acceptable implementation strategy.
The current adapter crates are safe boundary stubs. They compile the intended
ports and deterministic command contracts, but they do not create SDL windows,
GL contexts, GPU resources, shaders, draw calls, swapchains, or presents. They
must not be treated as backend readiness evidence.
To close the macOS backend requirement, choose and vendor/lock a maintained
safe facade stack, then implement:
- SDL event source, window creation, GL context lifecycle, drawable size and
present;
- GL shader compile/link, buffer/texture upload, render state, draw calls and
diagnostics;
- game/viewer composition roots using those adapters;
- hidden-window/offscreen macOS smoke tests and licensed local model/terrain
frame captures.
Until those are implemented, Desktop GL evidence may document external probes
only; it does not satisfy the permanent adapter requirement.
+26 -1
View File
@@ -110,7 +110,7 @@ impl std::error::Error for PathError {}
/// Returns [`PathError`] when the input is empty, absolute, contains an /// Returns [`PathError`] when the input is empty, absolute, contains an
/// embedded NUL, attempts parent traversal, or is not valid UTF-8 after /// embedded NUL, attempts parent traversal, or is not valid UTF-8 after
/// legacy separator normalization. /// legacy separator normalization.
pub fn normalize_relative(raw: &[u8], _policy: PathPolicy) -> Result<NormalizedPath, PathError> { pub fn normalize_relative(raw: &[u8], policy: PathPolicy) -> Result<NormalizedPath, PathError> {
if raw.is_empty() { if raw.is_empty() {
return Err(PathError::Empty); return Err(PathError::Empty);
} }
@@ -124,11 +124,17 @@ pub fn normalize_relative(raw: &[u8], _policy: PathPolicy) -> Result<NormalizedP
let mut parts = Vec::new(); let mut parts = Vec::new();
for part in text.split(['/', '\\']) { for part in text.split(['/', '\\']) {
if part.is_empty() || part == "." { if part.is_empty() || part == "." {
if policy == PathPolicy::StrictLegacy {
return Err(PathError::ParentTraversal);
}
continue; continue;
} }
if part == ".." { if part == ".." {
return Err(PathError::ParentTraversal); return Err(PathError::ParentTraversal);
} }
if policy == PathPolicy::StrictLegacy && part.contains(':') {
return Err(PathError::Absolute);
}
parts.push(part); parts.push(part);
} }
if parts.is_empty() { if parts.is_empty() {
@@ -223,6 +229,25 @@ mod tests {
); );
} }
#[test]
fn strict_legacy_rejects_host_only_segments() {
assert_eq!(
normalize_relative(b"./DATA/MAPS", PathPolicy::StrictLegacy),
Err(PathError::ParentTraversal)
);
assert_eq!(
normalize_relative(b"DATA//MAPS", PathPolicy::StrictLegacy),
Err(PathError::ParentTraversal)
);
assert_eq!(
normalize_relative(b"DATA/stream:name", PathPolicy::StrictLegacy),
Err(PathError::Absolute)
);
let host = normalize_relative(b"./DATA//MAPS", PathPolicy::HostCompatible).expect("host");
assert_eq!(host.as_str(), "DATA/MAPS");
}
#[test] #[test]
fn join_under_keeps_normalized_path_below_root() { fn join_under_keeps_normalized_path_below_root() {
let rel = normalize_relative(b"DATA/MAPS/Land.map", PathPolicy::StrictLegacy) let rel = normalize_relative(b"DATA/MAPS/Land.map", PathPolicy::StrictLegacy)
+123 -14
View File
@@ -1,7 +1,7 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! Virtual filesystem ports for resource loading. //! Virtual filesystem ports for resource loading.
use fparkan_path::{join_under, NormalizedPath}; use fparkan_path::{ascii_lookup_key, join_under, NormalizedPath};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -92,22 +92,27 @@ impl DirectoryVfs {
} }
fn host_path(&self, path: &NormalizedPath) -> Result<PathBuf, VfsError> { fn host_path(&self, path: &NormalizedPath) -> Result<PathBuf, VfsError> {
let exact = join_under(&self.root, path).map_err(|_| VfsError::Path)?; join_under(&self.root, path).map_err(|_| VfsError::Path)?;
if exact.exists() {
return Ok(exact);
}
resolve_casefolded(&self.root, path.as_str()) resolve_casefolded(&self.root, path.as_str())
} }
} }
impl Vfs for DirectoryVfs { impl Vfs for DirectoryVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> { fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
let meta = fs::metadata(self.host_path(path)?).map_err(VfsError::Io)?; let meta = fs::symlink_metadata(self.host_path(path)?).map_err(VfsError::Io)?;
Ok(metadata_from_fs(&meta)) Ok(metadata_from_fs(&meta))
} }
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> { fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
let bytes = fs::read(self.host_path(path)?).map_err(VfsError::Io)?; let host = self.host_path(path)?;
if fs::symlink_metadata(&host)
.map_err(VfsError::Io)?
.file_type()
.is_symlink()
{
return Err(VfsError::Path);
}
let bytes = fs::read(host).map_err(VfsError::Io)?;
Ok(Arc::from(bytes.into_boxed_slice())) Ok(Arc::from(bytes.into_boxed_slice()))
} }
@@ -115,7 +120,7 @@ impl Vfs for DirectoryVfs {
let base = self.host_path(prefix)?; let base = self.host_path(prefix)?;
let mut entries = Vec::new(); let mut entries = Vec::new();
if base.is_file() { if base.is_file() {
let metadata = fs::metadata(&base).map_err(VfsError::Io)?; let metadata = fs::symlink_metadata(&base).map_err(VfsError::Io)?;
entries.push(VfsEntry { entries.push(VfsEntry {
path: prefix.clone(), path: prefix.clone(),
metadata: metadata_from_fs(&metadata), metadata: metadata_from_fs(&metadata),
@@ -140,6 +145,9 @@ fn resolve_casefolded(root: &Path, normalized: &str) -> Result<PathBuf, VfsError
continue; continue;
}; };
if name.eq_ignore_ascii_case(segment) { if name.eq_ignore_ascii_case(segment) {
if entry.file_type().map_err(VfsError::Io)?.is_symlink() {
return Err(VfsError::Path);
}
matches.push(entry.path()); matches.push(entry.path());
} }
} }
@@ -175,7 +183,10 @@ fn list_recursive(root: &Path, dir: &Path, out: &mut Vec<VfsEntry>) -> Result<()
} }
children.sort(); children.sort();
for child in children { for child in children {
let metadata = fs::metadata(&child).map_err(VfsError::Io)?; let metadata = fs::symlink_metadata(&child).map_err(VfsError::Io)?;
if metadata.file_type().is_symlink() {
return Err(VfsError::Path);
}
if metadata.is_dir() { if metadata.is_dir() {
list_recursive(root, &child, out)?; list_recursive(root, &child, out)?;
continue; continue;
@@ -217,21 +228,51 @@ fn metadata_from_fs(metadata: &fs::Metadata) -> VfsMetadata {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct MemoryVfs { pub struct MemoryVfs {
files: BTreeMap<String, Arc<[u8]>>, files: BTreeMap<String, Arc<[u8]>>,
lookup: BTreeMap<Vec<u8>, Vec<String>>,
} }
impl MemoryVfs { impl MemoryVfs {
/// Inserts a file. /// Inserts a file.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) { pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) {
self.files.insert(path.as_str().to_string(), bytes); let path = path.as_str().to_string();
self.files.insert(path, bytes);
self.rebuild_lookup();
}
fn rebuild_lookup(&mut self) {
self.lookup.clear();
for path in self.files.keys() {
self.lookup
.entry(ascii_lookup_key(path.as_bytes()).0)
.or_default()
.push(path.clone());
}
for paths in self.lookup.values_mut() {
paths.sort();
}
}
fn resolve_path(&self, path: &NormalizedPath) -> Result<&str, VfsError> {
let key = ascii_lookup_key(path.as_str().as_bytes()).0;
let matches = self
.lookup
.get(&key)
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
match matches.as_slice() {
[single] => Ok(single.as_str()),
[] => Err(VfsError::NotFound(path.as_str().to_string())),
_ => Err(VfsError::Ambiguous(path.as_str().to_string())),
}
} }
} }
impl Vfs for MemoryVfs { impl Vfs for MemoryVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> { fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
let resolved = self.resolve_path(path)?;
let bytes = self let bytes = self
.files .files
.get(path.as_str()) .get(resolved)
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?; .ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
Ok(VfsMetadata { Ok(VfsMetadata {
len: bytes.len() as u64, len: bytes.len() as u64,
@@ -240,8 +281,9 @@ impl Vfs for MemoryVfs {
} }
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> { fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
let resolved = self.resolve_path(path)?;
self.files self.files
.get(path.as_str()) .get(resolved)
.cloned() .cloned()
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string())) .ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))
} }
@@ -384,6 +426,36 @@ mod tests {
std::fs::remove_dir_all(root).expect("cleanup"); std::fs::remove_dir_all(root).expect("cleanup");
} }
#[test]
fn directory_vfs_reports_casefold_ambiguity_even_for_exact_host_path() {
let root = unique_test_dir("casefold-ambiguous");
std::fs::create_dir_all(root.join("Data")).expect("mkdir first");
std::fs::create_dir_all(root.join("data")).expect("mkdir second");
std::fs::write(root.join("Data").join("File.bin"), b"first").expect("write first");
std::fs::write(root.join("data").join("File.bin"), b"second").expect("write second");
let collision_count = std::fs::read_dir(&root)
.expect("read root")
.flatten()
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case("data"))
})
.count();
if collision_count < 2 {
std::fs::remove_dir_all(root).expect("cleanup");
return;
}
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("path");
assert!(matches!(vfs.read(&path), Err(VfsError::Ambiguous(_))));
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test] #[test]
fn directory_vfs_lists_files_below_prefix() { fn directory_vfs_lists_files_below_prefix() {
let root = unique_test_dir("list"); let root = unique_test_dir("list");
@@ -403,6 +475,27 @@ mod tests {
std::fs::remove_dir_all(root).expect("cleanup"); std::fs::remove_dir_all(root).expect("cleanup");
} }
#[cfg(unix)]
#[test]
fn directory_vfs_rejects_symlink_escape() {
let root = unique_test_dir("symlink-escape");
let outside = unique_test_dir("symlink-outside");
std::fs::create_dir_all(&root).expect("mkdir root");
std::fs::create_dir_all(&outside).expect("mkdir outside");
std::fs::write(outside.join("secret.bin"), b"secret").expect("write outside");
std::os::unix::fs::symlink(&outside, root.join("DATA")).expect("symlink");
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"DATA/secret.bin", PathPolicy::StrictLegacy).expect("path");
let prefix = normalize_relative(b"DATA", PathPolicy::StrictLegacy).expect("prefix");
assert!(matches!(vfs.read(&path), Err(VfsError::Path)));
assert!(matches!(vfs.list(&prefix), Err(VfsError::Path)));
std::fs::remove_dir_all(root).expect("cleanup root");
std::fs::remove_dir_all(outside).expect("cleanup outside");
}
#[test] #[test]
fn casefold_selector_reports_ambiguous_segments() { fn casefold_selector_reports_ambiguous_segments() {
let err = select_casefolded_match( let err = select_casefolded_match(
@@ -417,7 +510,7 @@ mod tests {
} }
#[test] #[test]
fn memory_vfs_uses_exact_lookup() { fn memory_vfs_uses_ascii_casefold_lookup() {
let path = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("path"); let path = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("path");
let mut vfs = MemoryVfs::default(); let mut vfs = MemoryVfs::default();
vfs.insert(path.clone(), Arc::from(b"payload".as_slice())); vfs.insert(path.clone(), Arc::from(b"payload".as_slice()));
@@ -427,7 +520,23 @@ mod tests {
let other_case = let other_case =
normalize_relative(b"data/file.bin", PathPolicy::StrictLegacy).expect("path"); normalize_relative(b"data/file.bin", PathPolicy::StrictLegacy).expect("path");
assert!(matches!(vfs.read(&other_case), Err(VfsError::NotFound(_)))); assert_eq!(
vfs.read(&other_case).expect("casefold read").as_ref(),
b"payload"
);
}
#[test]
fn memory_vfs_reports_casefold_ambiguity() {
let first = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("first");
let second =
normalize_relative(b"DATA/file.BIN", PathPolicy::StrictLegacy).expect("second");
let query = normalize_relative(b"data/file.bin", PathPolicy::StrictLegacy).expect("query");
let mut vfs = MemoryVfs::default();
vfs.insert(first, Arc::from(b"first".as_slice()));
vfs.insert(second, Arc::from(b"second".as_slice()));
assert!(matches!(vfs.read(&query), Err(VfsError::Ambiguous(_))));
} }
#[test] #[test]
+13 -6
View File
@@ -114,16 +114,23 @@ key, configuration, device profile, initial state, input/time script и верс
## Local evidence requests ## Local evidence requests
На текущем рабочем месте закрыты статические, corpus и headless runtime gates. На текущем рабочем месте закрыты статические, corpus и headless runtime gates.
Для macOS Desktop GL подтверждены безопасный command/state trace и offscreen Для macOS Desktop GL есть только безопасный command/state trace и исторический
pixel capture: одноразовый offscreen pixel probe:
- `cargo test -p fparkan-render-gl --offline desktop_gl33_triangle_command_capture`; - `cargo test -p fparkan-render-gl --offline desktop_gl33_triangle_command_capture`;
- `fixtures/acceptance/macos-gl33-triangle-capture.json`. - `fixtures/acceptance/macos-gl33-triangle-capture.json`.
`S3-GL-001` считается закрытым для текущей macOS-focused цели: временный `S3-GL-001` не считается закрытым: временный `rustc` probe создал CGL/OpenGL
`rustc` probe создал CGL/OpenGL offscreen FBO, выполнил shader-based triangle offscreen FBO, выполнил shader-based triangle draw, прочитал RGBA pixels и
draw, прочитал RGBA pixels и сохранил hash capture. Probe не добавляет сохранил hash capture, но постоянный workspace adapter по-прежнему не создаёт
project-owned `unsafe` в workspace; постоянный adapter API остаётся safe. SDL window, GL context, GPU resources, shader programs, draw calls или present.
Probe не добавляет project-owned `unsafe` в workspace и остаётся только external
evidence request artifact.
Для повышения `S3-GL-001` до `covered` нужен постоянный macOS backend через
выбранную safe facade stack: SDL event/window/context lifecycle, Desktop GL 3.3
shader/buffer/texture/draw/present path, hidden-window/offscreen smoke test и
licensed local model/terrain frame capture.
Для повышения `S3-GL-002` до `covered` всё ещё нужен воспроизводимый GLES2 Для повышения `S3-GL-002` до `covered` всё ещё нужен воспроизводимый GLES2
backend profile: GLES2 должен создать кадр, сохранить pixel capture и тот же backend profile: GLES2 должен создать кадр, сохранить pixel capture и тот же
+2 -2
View File
@@ -21,7 +21,7 @@ S0-CORPUS-005 covered cargo test -p fparkan-corpus --offline fingerprint_changes
S0-CORPUS-006 covered cargo test -p fparkan-corpus --offline atomic_report_write S0-CORPUS-006 covered cargo test -p fparkan-corpus --offline atomic_report_write
S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_mapped S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_mapped
S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version
S0-GL-001 covered cargo test -p fparkan-platform-sdl -p fparkan-render-gl --offline adapter_reports_safe_project_layer_ready S0-GL-001 covered cargo test -p fparkan-platform-sdl -p fparkan-render-gl --offline adapter_boundary_is_project_owned_unsafe_free
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
@@ -220,7 +220,7 @@ S3-RENDER-006 covered cargo test -p fparkan-render --offline invalid_range_retur
S3-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable S3-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable
S3-RENDER-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures S3-RENDER-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures
S3-RENDER-009 covered cargo xtask policy S3-RENDER-009 covered cargo xtask policy
S3-GL-001 covered cargo test -p fparkan-render-gl --offline desktop_gl33_triangle_command_capture plus fixtures/acceptance/macos-gl33-triangle-capture.json records macOS CGL/OpenGL offscreen FBO pixel capture S3-GL-001 omitted permanent macOS Desktop GL 3.3 adapter is not implemented; historical CGL probe is retained as external evidence only
S3-GL-002 omitted outside the current macOS-focused goal scope; GLES2 remains documented for portable/non-macOS targets S3-GL-002 omitted outside the current macOS-focused goal scope; GLES2 remains documented for portable/non-macOS targets
S3-GL-003 covered cargo test -p fparkan-render-gl --offline shader_compile_failure_diagnostic_contains_profile_and_log S3-GL-003 covered cargo test -p fparkan-render-gl --offline shader_compile_failure_diagnostic_contains_profile_and_log
S3-VIEWER-001 covered cargo test -p fparkan-viewer --offline model_fixture_uses_viewer_service_and_render_commands S3-VIEWER-001 covered cargo test -p fparkan-viewer --offline model_fixture_uses_viewer_service_and_render_commands
1 # Acceptance coverage manifest.
21 S0-CORPUS-006
22 S0-CLI-001
23 S0-CLI-002
24 S0-GL-001
25 S0-LIMIT-001
26 S0-LIMIT-002
27 L1-P1-NRES-001
220 S3-RENDER-007
221 S3-RENDER-008
222 S3-RENDER-009
223 S3-GL-001
224 S3-GL-002
225 S3-GL-003
226 S3-VIEWER-001