fix: harden path lookup and mark gl backend gap
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#![forbid(unsafe_code)]
|
||||
//! SDL platform adapter proof behind safe `FParkan` ports.
|
||||
//! SDL platform adapter boundary stubs behind safe `FParkan` ports.
|
||||
|
||||
use fparkan_platform::{
|
||||
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]
|
||||
pub fn safe_adapter_ready() -> bool {
|
||||
pub fn project_owned_layer_unsafe_free() -> bool {
|
||||
SdlAdapterCapabilities::default().project_owned_unsafe_free
|
||||
}
|
||||
|
||||
/// In-memory event source used by adapter smoke tests and composition roots
|
||||
/// before a concrete SDL runtime is injected.
|
||||
/// In-memory event source used by adapter smoke tests before a concrete SDL
|
||||
/// runtime is selected.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SdlEventSourceProof {
|
||||
pub struct SdlEventSourceStub {
|
||||
pending: Vec<PlatformEvent>,
|
||||
}
|
||||
|
||||
impl SdlEventSourceProof {
|
||||
impl SdlEventSourceStub {
|
||||
/// Creates an event source with deterministic pending events.
|
||||
#[must_use]
|
||||
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> {
|
||||
out.append(&mut self.pending);
|
||||
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)]
|
||||
pub struct SdlWindowProof {
|
||||
pub struct SdlWindowStub {
|
||||
size: PhysicalSize,
|
||||
presents: u64,
|
||||
}
|
||||
|
||||
impl SdlWindowProof {
|
||||
/// Creates a proof window with a fixed drawable size.
|
||||
impl SdlWindowStub {
|
||||
/// Creates a stub window with a fixed drawable size.
|
||||
#[must_use]
|
||||
pub fn new(size: PhysicalSize) -> Self {
|
||||
Self { size, presents: 0 }
|
||||
@@ -82,7 +82,7 @@ impl SdlWindowProof {
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPort for SdlWindowProof {
|
||||
impl WindowPort for SdlWindowStub {
|
||||
fn drawable_size(&self) -> PhysicalSize {
|
||||
self.size
|
||||
}
|
||||
@@ -98,20 +98,20 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adapter_reports_safe_project_layer_ready() {
|
||||
assert!(safe_adapter_ready());
|
||||
fn adapter_boundary_is_project_owned_unsafe_free() {
|
||||
assert!(project_owned_layer_unsafe_free());
|
||||
assert_eq!(SdlAdapterCapabilities::default().graphics.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
source.poll(&mut events)?;
|
||||
source.poll(&mut events)?;
|
||||
assert_eq!(events, vec![PlatformEvent::Quit]);
|
||||
|
||||
let mut window = SdlWindowProof::new(PhysicalSize {
|
||||
let mut window = SdlWindowStub::new(PhysicalSize {
|
||||
width: 320,
|
||||
height: 240,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![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::{
|
||||
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]
|
||||
pub fn safe_adapter_ready() -> bool {
|
||||
pub fn project_owned_layer_unsafe_free() -> bool {
|
||||
GlAdapterCapabilities::default().project_owned_unsafe_free
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ pub fn compile_shader_source(
|
||||
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
|
||||
/// [`RenderBackend`] port once an audited safe GL facade is selected. This type
|
||||
@@ -147,8 +147,8 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn adapter_reports_safe_project_layer_ready() {
|
||||
assert!(safe_adapter_ready());
|
||||
fn adapter_boundary_is_project_owned_unsafe_free() {
|
||||
assert!(project_owned_layer_unsafe_free());
|
||||
assert_eq!(GlAdapterCapabilities::default().profiles.len(), 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,25 @@
|
||||
|
||||
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.
|
||||
|
||||
@@ -110,7 +110,7 @@ impl std::error::Error for PathError {}
|
||||
/// Returns [`PathError`] when the input is empty, absolute, contains an
|
||||
/// embedded NUL, attempts parent traversal, or is not valid UTF-8 after
|
||||
/// 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() {
|
||||
return Err(PathError::Empty);
|
||||
}
|
||||
@@ -124,11 +124,17 @@ pub fn normalize_relative(raw: &[u8], _policy: PathPolicy) -> Result<NormalizedP
|
||||
let mut parts = Vec::new();
|
||||
for part in text.split(['/', '\\']) {
|
||||
if part.is_empty() || part == "." {
|
||||
if policy == PathPolicy::StrictLegacy {
|
||||
return Err(PathError::ParentTraversal);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if part == ".." {
|
||||
return Err(PathError::ParentTraversal);
|
||||
}
|
||||
if policy == PathPolicy::StrictLegacy && part.contains(':') {
|
||||
return Err(PathError::Absolute);
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
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]
|
||||
fn join_under_keeps_normalized_path_below_root() {
|
||||
let rel = normalize_relative(b"DATA/MAPS/Land.map", PathPolicy::StrictLegacy)
|
||||
|
||||
+123
-14
@@ -1,7 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
//! 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::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -92,22 +92,27 @@ impl DirectoryVfs {
|
||||
}
|
||||
|
||||
fn host_path(&self, path: &NormalizedPath) -> Result<PathBuf, VfsError> {
|
||||
let exact = join_under(&self.root, path).map_err(|_| VfsError::Path)?;
|
||||
if exact.exists() {
|
||||
return Ok(exact);
|
||||
}
|
||||
join_under(&self.root, path).map_err(|_| VfsError::Path)?;
|
||||
resolve_casefolded(&self.root, path.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Vfs for DirectoryVfs {
|
||||
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))
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
@@ -115,7 +120,7 @@ impl Vfs for DirectoryVfs {
|
||||
let base = self.host_path(prefix)?;
|
||||
let mut entries = Vec::new();
|
||||
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 {
|
||||
path: prefix.clone(),
|
||||
metadata: metadata_from_fs(&metadata),
|
||||
@@ -140,6 +145,9 @@ fn resolve_casefolded(root: &Path, normalized: &str) -> Result<PathBuf, VfsError
|
||||
continue;
|
||||
};
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -175,7 +183,10 @@ fn list_recursive(root: &Path, dir: &Path, out: &mut Vec<VfsEntry>) -> Result<()
|
||||
}
|
||||
children.sort();
|
||||
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() {
|
||||
list_recursive(root, &child, out)?;
|
||||
continue;
|
||||
@@ -217,21 +228,51 @@ fn metadata_from_fs(metadata: &fs::Metadata) -> VfsMetadata {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MemoryVfs {
|
||||
files: BTreeMap<String, Arc<[u8]>>,
|
||||
lookup: BTreeMap<Vec<u8>, Vec<String>>,
|
||||
}
|
||||
|
||||
impl MemoryVfs {
|
||||
/// Inserts a file.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
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 {
|
||||
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
|
||||
let resolved = self.resolve_path(path)?;
|
||||
let bytes = self
|
||||
.files
|
||||
.get(path.as_str())
|
||||
.get(resolved)
|
||||
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
|
||||
Ok(VfsMetadata {
|
||||
len: bytes.len() as u64,
|
||||
@@ -240,8 +281,9 @@ impl Vfs for MemoryVfs {
|
||||
}
|
||||
|
||||
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
|
||||
let resolved = self.resolve_path(path)?;
|
||||
self.files
|
||||
.get(path.as_str())
|
||||
.get(resolved)
|
||||
.cloned()
|
||||
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))
|
||||
}
|
||||
@@ -384,6 +426,36 @@ mod tests {
|
||||
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]
|
||||
fn directory_vfs_lists_files_below_prefix() {
|
||||
let root = unique_test_dir("list");
|
||||
@@ -403,6 +475,27 @@ mod tests {
|
||||
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]
|
||||
fn casefold_selector_reports_ambiguous_segments() {
|
||||
let err = select_casefolded_match(
|
||||
@@ -417,7 +510,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 mut vfs = MemoryVfs::default();
|
||||
vfs.insert(path.clone(), Arc::from(b"payload".as_slice()));
|
||||
@@ -427,7 +520,23 @@ mod tests {
|
||||
|
||||
let other_case =
|
||||
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]
|
||||
|
||||
@@ -114,16 +114,23 @@ key, configuration, device profile, initial state, input/time script и верс
|
||||
## Local evidence requests
|
||||
|
||||
На текущем рабочем месте закрыты статические, corpus и headless runtime gates.
|
||||
Для macOS Desktop GL подтверждены безопасный command/state trace и offscreen
|
||||
pixel capture:
|
||||
Для macOS Desktop GL есть только безопасный command/state trace и исторический
|
||||
одноразовый offscreen pixel probe:
|
||||
|
||||
- `cargo test -p fparkan-render-gl --offline desktop_gl33_triangle_command_capture`;
|
||||
- `fixtures/acceptance/macos-gl33-triangle-capture.json`.
|
||||
|
||||
`S3-GL-001` считается закрытым для текущей macOS-focused цели: временный
|
||||
`rustc` probe создал CGL/OpenGL offscreen FBO, выполнил shader-based triangle
|
||||
draw, прочитал RGBA pixels и сохранил hash capture. Probe не добавляет
|
||||
project-owned `unsafe` в workspace; постоянный adapter API остаётся safe.
|
||||
`S3-GL-001` не считается закрытым: временный `rustc` probe создал CGL/OpenGL
|
||||
offscreen FBO, выполнил shader-based triangle draw, прочитал RGBA pixels и
|
||||
сохранил hash capture, но постоянный workspace adapter по-прежнему не создаёт
|
||||
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
|
||||
backend profile: GLES2 должен создать кадр, сохранить pixel capture и тот же
|
||||
|
||||
@@ -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-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-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-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
|
||||
@@ -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-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures
|
||||
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-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
|
||||
|
||||
|
Reference in New Issue
Block a user