Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ is `docs/dev/macos-moltenvk-decision.md`.

## Ready For Changelog

- [x] Linux save and configuration directories now work when an explicit home/save path contains an ordinary ellipsis (`...`) or another repeated-dot name. Directory creation still rejects a complete `..` parent component, preserving traversal protection without mistaking legitimate absolute paths for traversal attempts. This fixes the persistent-settings failure reported in GitHub issue #127.
- [x] Players can now choose smooth, sharp-mip, or deliberately pixelated material-texture sampling with the restored archived `image_filter` setting. All six retail Quake 4 filter names work on OpenGL and experimental Vulkan, changes update loaded textures immediately, and nearest-texel modes avoid anisotropic blur without overriding explicitly filtered HUD, font, shadow, or internal texture sampling. This resolves GitHub issue #121.
- [x] Milestone F now has a locally validated, default-off implementation for explicitly authored PBR materials, bounded OpenGL specular probes, and atomic OpenGL clustered decals. The 2026-08-23 current-source debug x64 evidence passed the static PBR/advanced-lighting checks, the final native 10/10 run, the final focused engine 2/2 rerun after an earlier 8/8 safe set, all four retail-PK4 compatibility roles, forced GL 3.3/4.1/4.3/4.5 PBR execution, the narrow validation-enabled Vulkan route, and exact `r_rendererModernQuality 0` rollback against leaf-disabled images. The stock baseline used all 40 retail PK4s with zero loose retail files; the generated PBR fixture remained temporary under `.tmp/`. PBR, probe, decal, and modern-visible controls stay default-off, `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` stays `0`, and this local implementation exit is not final committed-package/platform/driver promotion or broad authored probe/decal visual qualification.
- [x] Milestone F's remaining quality leaves are implemented independently and default off on OpenGL and Vulkan. Bounded view-aligned froxel integration, depth-normal SSR, and fixed eight-tap depth-derived SSGI share the native scene-colour/depth presentation tail without allocating temporal history for effect-only use. The stable eight-float contract, final 11/11 native suite, static backend checks, Vulkan shader pin, combined GL/Vulkan `game/airdefense1` gameplay, individual OpenGL leaves, visible enabled delta, and exact master-off zero packet pass locally with engine screenshots and clean API/error counters. `r_rendererModernQuality 0` remains the one-setting rollback. This is a scoped screen-space implementation, not shadowed light-injected volumetrics, roughness-aware G-buffer reflections, world-space GI, or whole-frame modern-lighting promotion.
Expand Down
30 changes: 28 additions & 2 deletions src/framework/FileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ static bool FS_IsWindowsDeviceQPathSegment( const char *segment, int segmentLeng
static_cast<unsigned char>( segment[ 4 ] ) == 0xB3 );
}

/*
========================
FS_HasParentOSPathSegment

Explicit OS paths may legitimately contain repeated dots in a filename or
directory name. Reject only a complete parent-directory component so an
absolute save path cannot be used to make CreateOSPath back up the hierarchy.
========================
*/
static bool FS_HasParentOSPathSegment( const char *OSPath ) {
const char *segmentStart = OSPath;
for ( const char *scan = OSPath; ; scan++ ) {
const char c = *scan;
if ( c != '\0' && c != '/' && c != '\\' ) {
continue;
}

if ( scan - segmentStart == 2 && segmentStart[ 0 ] == '.' && segmentStart[ 1 ] == '.' ) {
return true;
}
if ( c == '\0' ) {
return false;
}
segmentStart = scan + 1;
}
}

/*
========================
FS_ValidateRelativeWritePath
Expand Down Expand Up @@ -2028,8 +2055,7 @@ void idFileSystemLocal::CreateOSPath( const char *OSPath ) {
char *ofs;

// make absolutely sure that it can't back up the path
// FIXME: what about c: ?
if ( strstr( OSPath, ".." ) || strstr( OSPath, "::" ) ) {
if ( FS_HasParentOSPathSegment( OSPath ) || strstr( OSPath, "::" ) ) {
#ifdef _DEBUG
common->DPrintf( "refusing to create relative path \"%s\"\n", OSPath );
#endif
Expand Down
33 changes: 33 additions & 0 deletions tools/tests/filesystem_write_qpath_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def is_safe_relative_write_path_model(relative_path: str | None) -> bool:
return True


def has_parent_os_path_segment_model(os_path: str) -> bool:
return any(segment == ".." for segment in os_path.replace("\\", "/").split("/"))


def validate_behavior_model() -> None:
accepted = (
"openq4.cfg",
Expand Down Expand Up @@ -131,6 +135,26 @@ def validate_behavior_model() -> None:
if is_safe_relative_write_path_model(path):
raise AssertionError(f"Expected rejected relative mutation qpath: {path!r}")

accepted_os_paths = (
"/home/player/openQ4.../baseoq4/openQ4Config.cfg",
"/mnt/games/build_(openQ4,_Linux,_GOG_Assets...)/userdata/player/openQ4Config.cfg",
r"C:\Games\openQ4...\baseoq4\openQ4Config.cfg",
r"C:\Games\version..candidate\baseoq4\openQ4Config.cfg",
)
rejected_os_paths = (
"../outside.cfg",
"/home/player/../outside.cfg",
r"C:\Games\..\outside.cfg",
r"\\server\share\folder\..\outside.cfg",
"/home/player/..",
)
for path in accepted_os_paths:
if has_parent_os_path_segment_model(path):
raise AssertionError(f"Expected accepted explicit OS path: {path!r}")
for path in rejected_os_paths:
if not has_parent_os_path_segment_model(path):
raise AssertionError(f"Expected rejected explicit OS path: {path!r}")


def validate_generated_loadscreen_path_budget() -> None:
final_qpath = "guis/assets/generated/loadscreens/airdefense1_1820x1024.tga"
Expand Down Expand Up @@ -335,6 +359,7 @@ def validate_source_contract() -> None:
header = read("src/framework/FileSystem.h")

device_helper = function_body(source, "static bool FS_IsWindowsDeviceQPathSegment(")
parent_os_path_helper = function_body(source, "static bool FS_HasParentOSPathSegment(")
validator = function_body(source, "static bool FS_ValidateRelativeWritePath(")
open_write = function_body(source, "idFile *idFileSystemLocal::OpenFileWrite(")
open_append = function_body(source, "idFile *idFileSystemLocal::OpenFileAppend(")
Expand Down Expand Up @@ -367,6 +392,14 @@ def validate_source_contract() -> None:
require(device_helper, "digit == 0xB9", "superscript Windows device-name rejection")
require(device_helper, "digit == 0xC2", "UTF-8 superscript Windows device-name rejection")

require(parent_os_path_helper, "c != '/' && c != '\\\\'", "OS-path separator recognition")
require(parent_os_path_helper, "scan - segmentStart == 2", "exact parent-component length")
require(parent_os_path_helper, "segmentStart[ 0 ] == '.'", "parent-component first dot")
require(parent_os_path_helper, "segmentStart[ 1 ] == '.'", "parent-component second dot")
require(create_os_path, "FS_HasParentOSPathSegment( OSPath )", "explicit OS-path traversal rejection")
require(create_os_path, 'strstr( OSPath, "::" )', "legacy parent-path syntax rejection")
reject(create_os_path, 'strstr( OSPath, ".." )', "ordinary repeated dots remain valid")

for body, context, first_mutation in (
(write_file, "whole-file write API", "idFileSystemLocal::OpenFileWrite("),
(open_write, "relative write API", "BuildOSPath("),
Expand Down
Loading