fix: harden path lookup and mark gl backend gap
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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 и тот же
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Reference in New Issue
Block a user