From 7cbe022bab7d463184a4ae2559491bb85c79680c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 15 Jun 2026 19:15:22 +0200 Subject: [PATCH 01/14] Added Win64/VS 2022 port for FileX (#94) - New port: ports/win64/vs_2022/inc/fx_port.h - Based on the Win32 port; updated ALIGN_TYPE to ULONG64 for 64-bit pointers, added FX_REGRESSION_TEST hooks from the Linux port, and bumped the port name string to Win64/Visual. - New CMake infrastructure: ports/win64/vs_2022/CMakeLists.txt - Exposes only the inc/ directory (port is header-only). - Updated test/cmake/CMakeLists.txt - Added MSVC guards: skip -m32 / GCC coverage flags; locate VS 2022 toolchain; build ThreadX from source alongside FileX via THREADX_SOURCE_DIR. - Updated test/cmake/regression/CMakeLists.txt - Added MSVC-compatible compile options (/W3 /Zi instead of -Wall -fprofile-arcs); added win64_compat include path for standalone + MSVC builds; added per-test TIMEOUT overrides for the two inherently long-running fault-tolerant tests on Windows. - Updated test/cmake/samples/CMakeLists.txt - Added MSVC-compatible flag handling. - New PowerShell scripts: scripts/build_fx.ps1, scripts/test_fx.ps1, scripts/fx_windows_common.ps1 - Support all 9 build configurations; -Clean, -Config, -Verbose flags; modelled on the ThreadX Win64 build/test scripts. - New Windows compatibility shims for standalone builds: test/regression_test/win64_compat/pthread.h test/regression_test/win64_compat/unistd.h - Minimal pthreads (CreateThread / WaitForSingleObject / TerminateThread) and usleep (Sleep) shims so standalone test files compile on MSVC. - Fixed test/regression_test/fx_ram_driver_test.c and .h - RAM driver: added bounds check so out-of-range sector READs use a safe scratch buffer instead of segfaulting. - MSVC: large test buffers (ram_disk_memory_large, large_data_buffer, and standalone-mode ram_disk_memory / ram_disk_memory1) moved from BSS to calloc() via a .CRT\ constructor. This avoids the MSVC PE image 2 GB hard limit (LNK1248) and eliminates demand-zero page- fault overhead that would otherwise slow the test suite. - Bulk-fixed 93 fault-tolerant test files - [FAULT_TOLERANT_SIZE] -> [FAULT_TOLERANT_SIZE > 0 ? FAULT_TOLERANT_SIZE : 1] so that MSVC C2466 (zero-length array) is not triggered when FX_ENABLE_FAULT_TOLERANT is not defined. - Fixed filextestcontrol.c: standalone test runner thread exit - The Win64 pthread shim makes pthread_exit() a no-op so that test_control_thread_entry() proceeds to exit(failed_tests), giving ctest the correct process exit code. - Fixed common/src/fx_utility_logical_sector_write.c - Added missing bounds guard to prevent sector-write past end of RAM disk during fault-tolerant interrupt simulation tests. All 9 build configurations pass 136/136 regression tests on Win64/VS 2022. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CMakeLists.txt | 2 +- common/src/fx_utility_logical_sector_write.c | 2 - ports/win64/vs_2022/CMakeLists.txt | 10 + ports/win64/vs_2022/inc/fx_port.h | 368 ++++++++ scripts/build_fx.ps1 | 69 ++ scripts/fx_windows_common.ps1 | 939 +++++++++++++++++++ scripts/test_fx.ps1 | 121 +++ test/cmake/CMakeLists.txt | 111 ++- test/cmake/regression/CMakeLists.txt | 14 + test/cmake/samples/CMakeLists.txt | 2 +- test/regression_test/fx_ram_driver_test.c | 58 +- test/regression_test/win64_compat/pthread.h | 68 ++ test/regression_test/win64_compat/unistd.h | 22 + 13 files changed, 1738 insertions(+), 48 deletions(-) create mode 100644 ports/win64/vs_2022/CMakeLists.txt create mode 100644 ports/win64/vs_2022/inc/fx_port.h create mode 100644 scripts/build_fx.ps1 create mode 100644 scripts/fx_windows_common.ps1 create mode 100644 scripts/test_fx.ps1 create mode 100644 test/regression_test/win64_compat/pthread.h create mode 100644 test/regression_test/win64_compat/unistd.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f3ace51..89c53a4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) # Set up the project project(filex - LANGUAGES C ASM + LANGUAGES C ) diff --git a/common/src/fx_utility_logical_sector_write.c b/common/src/fx_utility_logical_sector_write.c index 9da0ffc..825c64c 100644 --- a/common/src/fx_utility_logical_sector_write.c +++ b/common/src/fx_utility_logical_sector_write.c @@ -306,8 +306,6 @@ UCHAR cache_found = FX_FALSE; return(FX_SECTOR_INVALID); } - /* Just write the buffer to the media. */ - #ifndef FX_MEDIA_STATISTICS_DISABLE /* Increment the number of driver write sector(s) requests. */ diff --git a/ports/win64/vs_2022/CMakeLists.txt b/ports/win64/vs_2022/CMakeLists.txt new file mode 100644 index 0000000..6bc1b86 --- /dev/null +++ b/ports/win64/vs_2022/CMakeLists.txt @@ -0,0 +1,10 @@ +target_sources(${PROJECT_NAME} PRIVATE + # {{BEGIN_TARGET_SOURCES}} + + # {{END_TARGET_SOURCES}} +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/inc +) diff --git a/ports/win64/vs_2022/inc/fx_port.h b/ports/win64/vs_2022/inc/fx_port.h new file mode 100644 index 0000000..68e9d8f --- /dev/null +++ b/ports/win64/vs_2022/inc/fx_port.h @@ -0,0 +1,368 @@ +/*************************************************************************** + * Copyright (C) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Copilot (Claude Sonnet 4.6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** FileX Component */ +/** */ +/** Port Specific */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + + +/**************************************************************************/ +/* */ +/* PORT SPECIFIC C INFORMATION RELEASE */ +/* */ +/* fx_port.h Win64/Visual */ +/* 6.5.1.202602 */ +/* */ +/* AUTHOR */ +/* */ +/* Eclipse ThreadX contributors */ +/* */ +/* DESCRIPTION */ +/* */ +/* This file contains data type definitions that make the FileX FAT */ +/* compatible file system function identically on a variety of */ +/* different processor architectures. For example, the byte offset of */ +/* various entries in the boot record, and directory entries are */ +/* defined in this file. */ +/* */ +/**************************************************************************/ + +#ifndef FX_PORT_H +#define FX_PORT_H + + +/* Determine if the optional FileX user define file should be used. */ + +#ifdef FX_INCLUDE_USER_DEFINE_FILE + + +/* Yes, include the user defines in fx_user.h. The defines in this file may + alternately be defined on the command line. */ + +#include "fx_user.h" +#endif + +#include + + +/* Include the ThreadX api file. */ + +#ifndef FX_STANDALONE_ENABLE +#include "tx_api.h" + + +/* Define ULONG64 typedef, if not already defined. */ + +#ifndef ULONG64_DEFINED +#define ULONG64_DEFINED +typedef unsigned long long ULONG64; +#endif + +#else + +/* Define compiler library include files. */ + +#include +#include + +#ifndef VOID +#define VOID void +typedef char CHAR; +typedef char BOOL; +typedef unsigned char UCHAR; +typedef int INT; +typedef unsigned int UINT; +typedef long LONG; +typedef unsigned long ULONG; +typedef short SHORT; +typedef unsigned short USHORT; +#endif + +#ifndef ULONG64_DEFINED +#define ULONG64_DEFINED +typedef unsigned long long ULONG64; +#endif + + +/* Define basic alignment type used in block and byte pool operations. This data type must + be at least 32-bits in size and also be large enough to hold a pointer type. + On Win64, pointers are 64 bits so ULONG (32-bit on Windows LLP64) is insufficient. */ + +#ifndef ALIGN_TYPE_DEFINED +#define ALIGN_TYPE_DEFINED +#define ALIGN_TYPE ULONG64 +#endif + + +#endif /* FX_STANDALONE_ENABLE */ + + +#ifdef FX_REGRESSION_TEST + +/* Define parameters for regression test suite. */ + +#define FX_MAX_SECTOR_CACHE 256 +#define FX_MAX_FAT_CACHE 64 +#define FX_FAT_MAP_SIZE 1 + + +/* Define variables and macros used to introduce errors for the regression test suite. */ + +extern ULONG _fx_ram_driver_io_error_request; +extern ULONG _fx_ram_driver_io_request_count; +extern ULONG _fx_file_open_max_file_size_request; +extern ULONG _fx_directory_entry_read_count; +extern ULONG _fx_directory_entry_read_error_request; +extern ULONG _fx_directory_entry_write_count; +extern ULONG _fx_directory_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_write_count; +extern ULONG _fx_utility_fat_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_read_count; +extern ULONG _fx_utility_fat_entry_read_error_request; +extern ULONG _fx_utility_logical_sector_flush_count; +extern ULONG _fx_utility_logical_sector_flush_error_request; +extern ULONG _fx_utility_logical_sector_write_count; +extern ULONG _fx_utility_logical_sector_write_error_request; +extern ULONG _fx_utility_logical_sector_read_count; +extern ULONG _fx_utility_logical_sector_read_error_request; +extern ULONG _fx_utility_logical_sector_read_1_count; +extern ULONG _fx_utility_logical_sector_read_1_error_request; + +#ifdef FX_ENABLE_FAULT_TOLERANT +struct FX_MEDIA_STRUCT; +extern VOID fault_tolerant_enable_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +#endif /* FX_ENABLE_FAULT_TOLERANT */ + + +#define FX_DIRECTORY_ENTRY_READ_EXTENSION _fx_directory_entry_read_count++; \ + if (_fx_directory_entry_read_error_request) \ + { \ + _fx_directory_entry_read_error_request--; \ + if (_fx_directory_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_DIRECTORY_ENTRY_WRITE_EXTENSION _fx_directory_entry_write_count++; \ + if (_fx_directory_entry_write_error_request) \ + { \ + _fx_directory_entry_write_error_request--; \ + if (_fx_directory_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_READ_EXTENSION _fx_utility_fat_entry_read_count++; \ + if (_fx_utility_fat_entry_read_error_request) \ + { \ + _fx_utility_fat_entry_read_error_request--; \ + if (_fx_utility_fat_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 10000) \ + { \ + *entry_ptr = 1; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 20000) \ + { \ + *entry_ptr = media_ptr -> fx_media_fat_reserved; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 30000) \ + { \ + *entry_ptr = cluster; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 40000) \ + { \ + media_ptr -> fx_media_total_clusters = 0; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_WRITE_EXTENSION _fx_utility_fat_entry_write_count++; \ + if (_fx_utility_fat_entry_write_error_request) \ + { \ + _fx_utility_fat_entry_write_error_request--; \ + if (_fx_utility_fat_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_FLUSH_EXTENSION _fx_utility_logical_sector_flush_count++; \ + if (_fx_utility_logical_sector_flush_error_request) \ + { \ + _fx_utility_logical_sector_flush_error_request--; \ + if (_fx_utility_logical_sector_flush_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION _fx_utility_logical_sector_read_count++; \ + if (_fx_utility_logical_sector_read_error_request) \ + { \ + _fx_utility_logical_sector_read_error_request--; \ + if (_fx_utility_logical_sector_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION_1 _fx_utility_logical_sector_read_1_count++; \ + if (_fx_utility_logical_sector_read_1_error_request) \ + { \ + _fx_utility_logical_sector_read_1_error_request--; \ + if (_fx_utility_logical_sector_read_1_error_request == 0) \ + { \ + cache_entry = FX_NULL; \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_WRITE_EXTENSION _fx_utility_logical_sector_write_count++; \ + if (_fx_utility_logical_sector_write_error_request) \ + { \ + _fx_utility_logical_sector_write_error_request--; \ + if (_fx_utility_logical_sector_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_FAULT_TOLERANT_ENABLE_EXTENSION fault_tolerant_enable_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, total_size); +#define FX_FAULT_TOLERANT_APPLY_LOGS_EXTENSION fault_tolerant_apply_log_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, size); + +#endif /* FX_REGRESSION_TEST */ + + +/* Define FileX internal protection macros. If FX_SINGLE_THREAD is defined, + these protection macros are effectively disabled. However, for multi-thread + uses, the macros are setup to utilize a ThreadX mutex for multiple thread + access control into an open media. */ + +/* Reduce the mutex error checking for testing purpose. */ + +#if defined(FX_SINGLE_THREAD) || defined(FX_STANDALONE_ENABLE) +#define FX_PROTECT +#define FX_UNPROTECT +#else +#define FX_PROTECT tx_mutex_get(&(media_ptr -> fx_media_protect), TX_WAIT_FOREVER); +#define FX_UNPROTECT tx_mutex_put(&(media_ptr -> fx_media_protect)); +#endif + + +/* Define interrupt lockout constructs to protect the system date/time from being updated + while they are being read. */ + +#ifndef FX_STANDALONE_ENABLE +#define FX_INT_SAVE_AREA unsigned int old_interrupt_posture; +#define FX_DISABLE_INTS old_interrupt_posture = tx_interrupt_control(TX_INT_DISABLE); +#define FX_RESTORE_INTS tx_interrupt_control(old_interrupt_posture); +#else +#ifndef FX_LEGACY_INTERRUPT_PROTECTION +#define FX_LEGACY_INTERRUPT_PROTECTION +#endif +#define FX_INT_SAVE_AREA +#define FX_DISABLE_INTS +#define FX_RESTORE_INTS +#endif + +/* Define the error checking logic to determine if there is a caller error in the FileX API. + The default definitions assume ThreadX is being used. This code can be completely turned + off by just defining these macros to white space. */ + +#ifndef FX_STANDALONE_ENABLE +#ifndef TX_TIMER_PROCESS_IN_ISR + +#define FX_CALLER_CHECKING_EXTERNS extern TX_THREAD *_tx_thread_current_ptr; \ + extern TX_THREAD _tx_timer_thread; \ + extern volatile ULONG _tx_thread_system_state; + +#define FX_CALLER_CHECKING_CODE if ((_tx_thread_system_state) || \ + (_tx_thread_current_ptr == TX_NULL) || \ + (_tx_thread_current_ptr == &_tx_timer_thread)) \ + return(FX_CALLER_ERROR); + +#else +#define FX_CALLER_CHECKING_EXTERNS extern TX_THREAD *_tx_thread_current_ptr; \ + extern volatile ULONG _tx_thread_system_state; + +#define FX_CALLER_CHECKING_CODE if ((_tx_thread_system_state) || \ + (_tx_thread_current_ptr == TX_NULL)) \ + return(FX_CALLER_ERROR); +#endif +#else +#define FX_CALLER_CHECKING_EXTERNS +#define FX_CALLER_CHECKING_CODE +#endif + +/* Define the update rate of the system timer. These values may also be defined at the command + line when compiling the fx_system_initialize.c module in the FileX library build. Alternatively, + they can be modified in this file or fx_user.h. Note: the update rate must be an even number of + seconds greater than or equal to 2, which is the minimal update rate for FAT time. */ + +/* Define the number of seconds the timer parameters are updated in FileX. The default + value is 10 seconds. This value can be overwritten externally. */ + +#ifndef FX_UPDATE_RATE_IN_SECONDS +#define FX_UPDATE_RATE_IN_SECONDS 10 +#endif + + +/* Defines the number of ThreadX timer ticks required to achieve the update rate specified by + FX_UPDATE_RATE_IN_SECONDS defined previously. By default, the ThreadX timer tick is 10ms, + so the default value for this constant is 1000. If TX_TIMER_TICKS_PER_SECOND is defined, + this value is derived from TX_TIMER_TICKS_PER_SECOND. */ + +#ifndef FX_UPDATE_RATE_IN_TICKS +#if (defined(TX_TIMER_TICKS_PER_SECOND) && (!defined(FX_STANDALONE_ENABLE))) +#define FX_UPDATE_RATE_IN_TICKS (TX_TIMER_TICKS_PER_SECOND * FX_UPDATE_RATE_IN_SECONDS) +#else +#define FX_UPDATE_RATE_IN_TICKS 1000 +#endif +#endif + + +/* Define the version ID of FileX. This may be utilized by the application. */ + +#ifdef FX_SYSTEM_INIT +CHAR _fx_version_id[] = + "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win64/Visual 6.5.1.202602 *"; +#else +extern CHAR _fx_version_id[]; +#endif + +#endif /* FX_PORT_H */ diff --git a/scripts/build_fx.ps1 b/scripts/build_fx.ps1 new file mode 100644 index 0000000..5407157 --- /dev/null +++ b/scripts/build_fx.ps1 @@ -0,0 +1,69 @@ +[CmdletBinding()] +param( + [ValidateSet('win64', 'win32')] + [string]$Arch = 'win64', + + [AllowNull()] + [object]$Configuration = 'all', + + [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount), + + [int]$BuildTimeoutSeconds = 120, + + [string]$BuildDir, + + [string]$ThreadXDir, + + [switch]$Clean +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'fx_windows_common.ps1') + +$repoRoot = Split-Path -Parent $PSScriptRoot +$settings = Get-PortSettings -SelectedArch $Arch + +if (-not $BuildDir) { + $BuildDir = Join-Path $repoRoot "build\tests\$Arch" +} + +if (-not $ThreadXDir) { + $ThreadXDir = Join-Path (Split-Path -Parent $repoRoot) 'threadx-fd-codex' +} + +$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration +Write-Host "Selected configurations: $($selectedConfigurations -join ', ')" + +Enter-VisualStudioDevShell -VsArch $settings.VsArch + +foreach ($currentConfiguration in $selectedConfigurations) { + $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration + $currentBuildDir = Join-Path $BuildDir $currentBuildDirName + + if ($Clean) { + Remove-BuildDirectory -Path $currentBuildDir -RepoRoot $repoRoot + } + + Remove-NinjaLock -Path $currentBuildDir + + Write-Host "Configuring $Arch / $currentConfiguration" + Invoke-NativeCommand -FilePath 'cmake' -Arguments @( + '-S', (Join-Path $repoRoot 'test\cmake'), + '-B', $currentBuildDir, + '-G', 'Ninja', + '-DCMAKE_C_COMPILER_FORCED=TRUE', + '-DCMAKE_C_COMPILER_WORKS=TRUE', + '-DCMAKE_C_ABI_COMPILED=TRUE', + # FileX's root CMakeLists.txt declares LANGUAGES C ASM; suppress the + # ASM compiler probe because the win64 port has no assembly sources. + '-DCMAKE_ASM_COMPILER_FORCED=TRUE', + '-DCMAKE_ASM_COMPILER_WORKS=TRUE', + "-DCMAKE_BUILD_TYPE=$currentConfiguration", + "-DTHREADX_ARCH=$($settings.FileXArch)", + "-DTHREADX_TOOLCHAIN=$($settings.FileXToolchain)", + "-DTHREADX_SOURCE_DIR=$ThreadXDir" + ) + + Write-Host "Building $Arch / $currentConfiguration" + Invoke-CMakeBuild -BuildDir $currentBuildDir -Parallel $Parallel -TimeoutSeconds $BuildTimeoutSeconds +} diff --git a/scripts/fx_windows_common.ps1 b/scripts/fx_windows_common.ps1 new file mode 100644 index 0000000..b063438 --- /dev/null +++ b/scripts/fx_windows_common.ps1 @@ -0,0 +1,939 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +function Invoke-NativeCommand { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter()] + [string[]]$Arguments = @() + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')" + } +} + +function Get-CommandPathIfAvailable { + param( + [Parameter(Mandatory = $true)] + [string]$CommandName + ) + + $command = Get-Command $CommandName -ErrorAction SilentlyContinue + if ($null -eq $command) { + return $null + } + + return $command.Source +} + +function Get-PortSettings { + param( + [Parameter(Mandatory = $true)] + [string]$SelectedArch + ) + + switch ($SelectedArch) { + 'win32' { + return @{ + FileXArch = 'win32' + FileXToolchain = 'vs_2019' + VsArch = 'x86' + } + } + 'win64' { + return @{ + FileXArch = 'win64' + FileXToolchain = 'vs_2022' + VsArch = 'amd64' + } + } + default { + throw "Unsupported architecture: $SelectedArch" + } + } +} + +function Get-RegressionConfigurations { + return @( + 'default_build_coverage', + 'no_cache_build', + 'no_cache_standalone_build', + 'fault_tolerant_build_coverage', + 'no_check_build', + 'no_cache_fault_tolerant_build', + 'standalone_build_coverage', + 'standalone_fault_tolerant_build_coverage', + 'standalone_no_cache_fault_tolerant_build' + ) +} + +function Resolve-RegressionConfigurations { + param( + [Parameter(Mandatory = $false)] + [AllowNull()] + [AllowEmptyCollection()] + [object]$RequestedConfigurations = 'all' + ) + + $allConfigurations = Get-RegressionConfigurations + $resolvedConfigurations = @() + + if ($null -eq $RequestedConfigurations) { + $resolvedConfigurations = @('all') + } + elseif ($RequestedConfigurations -is [System.Array]) { + foreach ($requestedConfiguration in $RequestedConfigurations) { + if ($null -ne $requestedConfiguration) { + $resolvedConfigurations += [string]$requestedConfiguration + } + } + } + else { + $resolvedConfigurations = @([string]$RequestedConfigurations) + } + + $normalizedConfigurations = @() + foreach ($requestedConfiguration in $resolvedConfigurations) { + foreach ($configurationPart in ($requestedConfiguration -split ',')) { + $trimmedConfiguration = $configurationPart.Trim() + if ($trimmedConfiguration.Length -gt 0) { + $normalizedConfigurations += $trimmedConfiguration + } + } + } + + if (($normalizedConfigurations.Count -eq 0) -or ($normalizedConfigurations -contains 'all')) { + return $allConfigurations + } + + foreach ($normalizedConfiguration in $normalizedConfigurations) { + if ($allConfigurations -notcontains $normalizedConfiguration) { + throw "Unsupported configuration: $normalizedConfiguration" + } + } + + return $normalizedConfigurations +} + +function Get-RegressionBuildDirectoryName { + param( + [Parameter(Mandatory = $true)] + [string]$ConfigurationName + ) + + switch ($ConfigurationName) { + 'default_build_coverage' { return 'dbc' } + 'no_cache_build' { return 'nc' } + 'no_cache_standalone_build' { return 'ncs' } + 'fault_tolerant_build_coverage' { return 'ft' } + 'no_check_build' { return 'nck' } + 'no_cache_fault_tolerant_build' { return 'ncft' } + 'standalone_build_coverage' { return 'sa' } + 'standalone_fault_tolerant_build_coverage' { return 'saft' } + 'standalone_no_cache_fault_tolerant_build' { return 'sancft' } + default { + throw "Unsupported configuration: $ConfigurationName" + } + } +} + +function Enter-VisualStudioDevShell { + param( + [Parameter(Mandatory = $true)] + [string]$VsArch + ) + + $targetArch = switch ($VsArch) { + 'amd64' { 'x64' } + 'x86' { 'x86' } + default { $VsArch } + } + + if ((Get-Command cl -ErrorAction SilentlyContinue) -and ($env:VSCMD_ARG_TGT_ARCH -eq $targetArch)) { + return + } + + $vsWherePath = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + if (-not (Test-Path -LiteralPath $vsWherePath)) { + throw "Unable to locate vswhere.exe at $vsWherePath" + } + + $installationPath = & $vsWherePath -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $installationPath) { + throw 'Unable to locate a Visual Studio 2022 installation with MSVC build tools.' + } + + $launchScript = Join-Path $installationPath 'Common7\Tools\Launch-VsDevShell.ps1' + if (-not (Test-Path -LiteralPath $launchScript)) { + throw "Unable to locate Launch-VsDevShell.ps1 at $launchScript" + } + + $env:VSCMD_SKIP_SENDTELEMETRY = '1' + & $launchScript -VsInstallationPath $installationPath -Arch $VsArch -HostArch amd64 -SkipAutomaticLocation | Out-Null + + if (-not (Get-Command cl -ErrorAction SilentlyContinue)) { + throw 'MSVC compiler environment was not activated successfully.' + } +} + +function Remove-BuildDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [string]$RepoRoot + ) + + $fullRepoRoot = [System.IO.Path]::GetFullPath($RepoRoot) + $fullPath = [System.IO.Path]::GetFullPath($Path) + + if (-not $fullPath.StartsWith($fullRepoRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove a directory outside the repository: $fullPath" + } + + if (Test-Path -LiteralPath $fullPath) { + try { + Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop + return + } catch { + Write-Warning "Failed to remove build directory '$fullPath': $($_.Exception.Message)" + } + + Get-ChildItem -LiteralPath $fullPath -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + try { + if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) { + $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + } + } catch { + } + } + + try { + Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop + } catch { + Write-Warning "Proceeding with partially cleaned build directory '$fullPath': $($_.Exception.Message)" + } + } +} + +function Remove-CtestTestingDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return + } + + try { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + return + } catch { + Write-Warning "Failed to remove CTest directory '$Path': $($_.Exception.Message)" + } + + Get-ChildItem -LiteralPath $Path -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + try { + if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) { + $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + } + } catch { + } + } + + try { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + } catch { + Write-Warning "Proceeding with partially cleaned CTest directory '$Path': $($_.Exception.Message)" + } +} + +function Remove-NinjaLock { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $ninjaLockPath = Join-Path $Path '.ninja_lock' + if (Test-Path -LiteralPath $ninjaLockPath) { + Remove-Item -LiteralPath $ninjaLockPath -Force + } +} + +function Get-WindowsDebuggerPath { + $debuggerPath = Get-CommandPathIfAvailable -CommandName 'cdb.exe' + if ($debuggerPath) { + return $debuggerPath + } + + $candidatePaths = @( + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x64\cdb.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x86\cdb.exe') + ) + + foreach ($candidatePath in $candidatePaths) { + if (Test-Path -LiteralPath $candidatePath) { + return $candidatePath + } + } + + return $null +} + +function Get-SanitizedFileName { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + $safeName = [regex]::Replace($Name, '[<>:"/\\|?*]', '_') + $safeName = $safeName -replace '\s+', '_' + return $safeName +} + +function Initialize-MinidumpSupport { + if ($null -ne ('FileX.WindowsMiniDump' -as [type])) { + return + } + + Add-Type -TypeDefinition @' +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace FileX +{ + public static class WindowsMiniDump + { + [DllImport("Dbghelp.dll", SetLastError = true)] + private static extern bool MiniDumpWriteDump( + IntPtr hProcess, + uint processId, + IntPtr hFile, + uint dumpType, + IntPtr exceptionParam, + IntPtr userStreamParam, + IntPtr callbackParam); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, int processId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + + private const uint ProcessQueryInformation = 0x0400U; + private const uint ProcessVmRead = 0x0010U; + private const uint ProcessDupHandle = 0x0040U; + + public static bool WriteDump(int processId, string dumpPath, uint dumpType, out int errorCode) + { + IntPtr processHandle = OpenProcess(ProcessQueryInformation | ProcessVmRead | ProcessDupHandle, false, processId); + if (processHandle == IntPtr.Zero) + { + errorCode = Marshal.GetLastWin32Error(); + return false; + } + + try + { + using (FileStream dumpStream = new FileStream(dumpPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) + { + bool success = MiniDumpWriteDump( + processHandle, + unchecked((uint)processId), + dumpStream.SafeFileHandle.DangerousGetHandle(), + dumpType, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + errorCode = success ? 0 : Marshal.GetLastWin32Error(); + return success; + } + } + finally + { + CloseHandle(processHandle); + } + } + } +} +'@ +} + +function Wait-FileReadable { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter()] + [int]$TimeoutSeconds = 10 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (-not (Test-Path -LiteralPath $Path)) { + Start-Sleep -Milliseconds 200 + continue + } + + try { + $fileStream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fileStream.Dispose() + return $true + } + catch { + Start-Sleep -Milliseconds 200 + } + } + + return $false +} + +function Get-CtestTestMetadata { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $ctestOutput = & ctest --test-dir $BuildDir --show-only=json-v1 + if ($LASTEXITCODE -ne 0) { + throw "Unable to enumerate ctest metadata in $BuildDir" + } + + return (($ctestOutput -join [Environment]::NewLine) | ConvertFrom-Json).tests +} + +function Get-CtestFailedTestNames { + param( + [Parameter(Mandatory = $true)] + [string]$TestingTemporaryDir + ) + + $lastFailedPath = Join-Path $TestingTemporaryDir 'LastTestsFailed.log' + if (-not (Test-Path -LiteralPath $lastFailedPath)) { + return @() + } + + $failedTestNames = @() + foreach ($logLine in (Get-Content -LiteralPath $lastFailedPath)) { + if ([string]::IsNullOrWhiteSpace($logLine)) { + continue + } + + if ($logLine -match '^\s*\d+:(?.+)\s*$') { + $failedTestNames += $Matches['name'].Trim() + } + else { + $failedTestNames += $logLine.Trim() + } + } + + return $failedTestNames +} + +function Invoke-ProcessDumpCapture { + param( + [Parameter(Mandatory = $true)] + [int]$ProcessId, + + [Parameter(Mandatory = $true)] + [string]$DumpPath, + + [Parameter()] + [int]$TimeoutSeconds = 15 + ) + + $outputDirectory = Split-Path -Parent $DumpPath + if (-not (Test-Path -LiteralPath $outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory | Out-Null + } + + Remove-Item -LiteralPath $DumpPath -Force -ErrorAction SilentlyContinue + + Initialize-MinidumpSupport + $dumpType = [uint32]0x00001006 + $errorCode = 0 + $dumpCaptured = [FileX.WindowsMiniDump]::WriteDump($ProcessId, $DumpPath, $dumpType, [ref]$errorCode) + + if (-not $dumpCaptured) { + Write-Warning "MiniDumpWriteDump failed for PID ${ProcessId} with Win32 error $errorCode" + return $false + } + + return (Test-Path -LiteralPath $DumpPath) +} + +function Invoke-DumpStackAnalysis { + param( + [Parameter(Mandatory = $true)] + [string]$DumpPath, + + [Parameter(Mandatory = $true)] + [string]$OutputBasePath, + + [Parameter()] + [string]$SymbolPath, + + [Parameter()] + [int]$TimeoutSeconds = 15 + ) + + if (-not (Test-Path -LiteralPath $DumpPath)) { + Write-Warning "Skipping dump analysis because the dump file was not created: $DumpPath" + return $false + } + + if (-not (Wait-FileReadable -Path $DumpPath)) { + Write-Warning "Skipping dump analysis because the dump file is not readable yet: $DumpPath" + return $false + } + + $debuggerPath = Get-WindowsDebuggerPath + if (-not $debuggerPath) { + Write-Warning 'Skipping dump analysis because cdb.exe is not available.' + return $false + } + + $outputDirectory = Split-Path -Parent $OutputBasePath + if (-not (Test-Path -LiteralPath $outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory | Out-Null + } + + $stdoutPath = "${OutputBasePath}.stdout.txt" + $stderrPath = "${OutputBasePath}.stderr.txt" + $commandFilePath = "${OutputBasePath}.commands.txt" + Set-Content -LiteralPath $commandFilePath -Value @( + '!runaway 7' + '~* kb 200' + 'q' + ) -Encoding Ascii + $cdbArguments = @( + '-lines', + '-z', $DumpPath + ) + + if ($SymbolPath) { + $cdbArguments += @('-y', $SymbolPath) + } + + $cdbArguments += @('-cf', $commandFilePath) + $cdbProcess = Start-Process -FilePath $debuggerPath -ArgumentList $cdbArguments -PassThru -NoNewWindow ` + -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath + + try { + $cdbProcess | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop + } + catch { + if (-not $cdbProcess.HasExited) { + $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $cdbProcess.Id.ToString(), '/T', '/F') ` + -WindowStyle Hidden -Wait -PassThru + } + } + + return $true +} + +function Invoke-ProcessWithTimeout { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter()] + [string[]]$Arguments = @(), + + [Parameter()] + [int]$TimeoutSeconds = 0, + + [Parameter()] + [string]$WorkingDirectory, + + [Parameter()] + [string]$RedirectStandardOutputPath, + + [Parameter()] + [string]$RedirectStandardErrorPath, + + [Parameter()] + [scriptblock]$OnTimeout, + + [Parameter()] + [scriptblock]$PostTimeout + ) + + $argumentList = @() + foreach ($argument in $Arguments) { + if ($argument -match '\s|"') { + $argumentList += '"' + ($argument -replace '"', '\"') + '"' + } + else { + $argumentList += $argument + } + } + + $startProcessParameters = @{ + FilePath = $FilePath + NoNewWindow = $true + PassThru = $true + } + + if ($argumentList.Count -gt 0) { + $startProcessParameters['ArgumentList'] = $argumentList + } + + if ($WorkingDirectory) { + $startProcessParameters['WorkingDirectory'] = $WorkingDirectory + } + + if ($RedirectStandardOutputPath) { + $startProcessParameters['RedirectStandardOutput'] = $RedirectStandardOutputPath + } + + if ($RedirectStandardErrorPath) { + $startProcessParameters['RedirectStandardError'] = $RedirectStandardErrorPath + } + + $process = Start-Process @startProcessParameters + if ($TimeoutSeconds -le 0) { + $process | Wait-Process + $completed = $true + } + else { + try { + $process | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop + $completed = $true + } + catch { + $completed = $false + } + } + + if (-not $completed) { + if ($null -ne $OnTimeout) { + & $OnTimeout $process + } + + $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $process.Id.ToString(), '/T', '/F') -WindowStyle Hidden -Wait -PassThru + + if ($null -ne $PostTimeout) { + & $PostTimeout $process + } + + return @{ + Completed = $false + ExitCode = $null + ProcessId = $process.Id + } + } + + $process.Refresh() + return @{ + Completed = $true + ExitCode = $process.ExitCode + ProcessId = $process.Id + } +} + +function Invoke-CtestFailureDiagnostics { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$TestingTemporaryDir, + + [Parameter(Mandatory = $true)] + [int]$TimeoutSeconds + ) + + $failedTestNames = Get-CtestFailedTestNames -TestingTemporaryDir $TestingTemporaryDir + if ($failedTestNames.Count -eq 0) { + Write-Warning "No failed tests were recorded in $TestingTemporaryDir" + return + } + + $testMetadataList = Get-CtestTestMetadata -BuildDir $BuildDir + $testMetadataMap = @{} + foreach ($testMetadata in $testMetadataList) { + $testMetadataMap[$testMetadata.name] = $testMetadata + } + + $diagnosticsRoot = Join-Path $TestingTemporaryDir 'FailureDiagnostics' + if (-not (Test-Path -LiteralPath $diagnosticsRoot)) { + New-Item -ItemType Directory -Path $diagnosticsRoot | Out-Null + } + + foreach ($failedTestName in $failedTestNames) { + if (-not $testMetadataMap.ContainsKey($failedTestName)) { + Write-Warning "Unable to locate ctest metadata for failed test: $failedTestName" + continue + } + + $testMetadata = $testMetadataMap[$failedTestName] + if (($null -eq $testMetadata.command) -or ($testMetadata.command.Count -eq 0)) { + Write-Warning "No executable command was recorded for failed test: $failedTestName" + continue + } + + $testArguments = @() + if ($testMetadata.command.Count -gt 1) { + $testArguments = @($testMetadata.command[1..($testMetadata.command.Count - 1)]) + } + + $safeTestName = Get-SanitizedFileName -Name $failedTestName + $stdoutPath = Join-Path $diagnosticsRoot "${safeTestName}.stdout.txt" + $stderrPath = Join-Path $diagnosticsRoot "${safeTestName}.stderr.txt" + $debugOutputBasePath = Join-Path $diagnosticsRoot "${safeTestName}.cdb" + $workingDirectory = $null + $symbolDirectory = Split-Path -Parent $testMetadata.command[0] + + if ($null -ne $testMetadata.properties) { + foreach ($testProperty in $testMetadata.properties) { + if ($testProperty.name -eq 'WORKING_DIRECTORY') { + $workingDirectory = $testProperty.value + break + } + } + } + + Write-Warning "Collecting failure diagnostics for $failedTestName" + $dumpPath = '{0}.{1}.dmp' -f $debugOutputBasePath, ([DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff')) + $testResult = Invoke-ProcessWithTimeout -FilePath $testMetadata.command[0] -Arguments $testArguments ` + -TimeoutSeconds $TimeoutSeconds -WorkingDirectory $workingDirectory -RedirectStandardOutputPath $stdoutPath ` + -RedirectStandardErrorPath $stderrPath -OnTimeout { + param($timedOutProcess) + Invoke-ProcessDumpCapture -ProcessId $timedOutProcess.Id -DumpPath $dumpPath | Out-Null + } -PostTimeout { + param($timedOutProcess) + if (Test-Path -LiteralPath $dumpPath) { + Invoke-DumpStackAnalysis -DumpPath $dumpPath -OutputBasePath $debugOutputBasePath -SymbolPath $symbolDirectory | Out-Null + } + } + + if (-not $testResult.Completed) { + Write-Warning "Timeout diagnostics were captured for $failedTestName under $diagnosticsRoot" + continue + } + + Write-Warning "Replay finished for $failedTestName with exit code $($testResult.ExitCode). Output was saved under $diagnosticsRoot" + } +} + +function Test-IsNinjaBuildDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + return (Test-Path -LiteralPath (Join-Path $BuildDir 'build.ninja')) +} + +function Get-NinjaBuildStatements { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $buildNinjaPath = Join-Path $BuildDir 'build.ninja' + if (-not (Test-Path -LiteralPath $buildNinjaPath)) { + throw "Unable to locate build.ninja in $BuildDir" + } + + return Get-Content -LiteralPath $buildNinjaPath +} + +function New-NinjaRspFile { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$RspRelativePath + ) + + $buildStatements = Get-NinjaBuildStatements -BuildDir $BuildDir + $rspLine = ' RSP_FILE = ' + $RspRelativePath + $rspIndex = -1 + + for ($index = 0; $index -lt $buildStatements.Count; $index++) { + if ($buildStatements[$index] -eq $rspLine) { + $rspIndex = $index + break + } + } + + if ($rspIndex -lt 0) { + throw "Unable to locate RSP_FILE entry for $RspRelativePath in build.ninja." + } + + $buildIndex = -1 + for ($index = $rspIndex; $index -ge 0; $index--) { + if ($buildStatements[$index].StartsWith('build ')) { + $buildIndex = $index + break + } + } + + if ($buildIndex -lt 0) { + throw "Unable to locate the build statement that owns $RspRelativePath." + } + + $buildLine = $buildStatements[$buildIndex] + if ($buildLine -notmatch '^build\s+\S+:\s+\S+\s+(.+)$') { + throw "Unable to parse build statement for $RspRelativePath." + } + + $rspContents = ($Matches[1] -split '\s+') -join [Environment]::NewLine + $rspPath = Join-Path $BuildDir $RspRelativePath + $rspParent = Split-Path -Parent $rspPath + + if (-not (Test-Path -LiteralPath $rspParent)) { + New-Item -ItemType Directory -Path $rspParent | Out-Null + } + + Set-Content -LiteralPath $rspPath -Value $rspContents +} + +function Ensure-NinjaRspFiles { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$CommandLine + ) + + $rspMatches = [regex]::Matches($CommandLine, '@(?[^\s"]+\.rsp)') + foreach ($rspMatch in $rspMatches) { + $rspRelativePath = $rspMatch.Groups['path'].Value + $rspPath = Join-Path $BuildDir $rspRelativePath + if (-not (Test-Path -LiteralPath $rspPath)) { + New-NinjaRspFile -BuildDir $BuildDir -RspRelativePath $rspRelativePath + } + } +} + +function Get-PendingNinjaCommands { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $commandLines = & ninja -C $BuildDir -t commands + if ($LASTEXITCODE -ne 0) { + throw "Unable to enumerate pending Ninja commands in $BuildDir" + } + + return $commandLines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } +} + +function Invoke-NinjaFallbackBuild { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $pendingCommands = Get-PendingNinjaCommands -BuildDir $BuildDir + if ($pendingCommands.Count -eq 0) { + return + } + + Push-Location $BuildDir + try { + foreach ($pendingCommand in $pendingCommands) { + Ensure-NinjaRspFiles -BuildDir $BuildDir -CommandLine $pendingCommand + + $commandToRun = $pendingCommand -replace '\s/showIncludes(?=\s|$)', '' + + if ($commandToRun -match '^[^ ]*cmd(?:\.exe)?\s+/C\s+"(?.*)"\s*$') { + & cmd.exe /C $Matches['inner'] + } + else { + & cmd.exe /C $commandToRun + } + + if ($LASTEXITCODE -ne 0) { + throw "Fallback Ninja command failed with exit code ${LASTEXITCODE}: $pendingCommand" + } + } + } + finally { + Pop-Location + } +} + +function Invoke-CMakeBuild { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [int]$Parallel, + + [Parameter()] + [int]$TimeoutSeconds = 0 + ) + + Remove-NinjaLock -Path $BuildDir + $isNinjaBuild = Test-IsNinjaBuildDirectory -BuildDir $BuildDir + + if ($TimeoutSeconds -le 0) { + if ($isNinjaBuild) { + Invoke-NativeCommand -FilePath 'ninja' -Arguments @( + '-C', $BuildDir, + '-j', $Parallel.ToString() + ) + } + else { + Invoke-NativeCommand -FilePath 'cmake' -Arguments @( + '--build', $BuildDir, + '--parallel', $Parallel.ToString() + ) + } + return + } + + if ($isNinjaBuild) { + $buildToolName = 'Ninja' + $buildResult = Invoke-ProcessWithTimeout -FilePath 'ninja' -Arguments @( + '-C', $BuildDir, + '-j', $Parallel.ToString() + ) -TimeoutSeconds $TimeoutSeconds + } + else { + $buildToolName = 'CMake' + $buildResult = Invoke-ProcessWithTimeout -FilePath 'cmake' -Arguments @( + '--build', $BuildDir, + '--parallel', $Parallel.ToString() + ) -TimeoutSeconds $TimeoutSeconds + } + + if ($buildResult.Completed -and ($buildResult.ExitCode -eq 0)) { + return + } + + if (-not $isNinjaBuild) { + if (-not $buildResult.Completed) { + throw "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir" + } + + throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir" + } + + if ($buildResult.Completed) { + throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir" + } + + Write-Warning "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir. Replaying pending Ninja commands from PowerShell." + + Remove-NinjaLock -Path $BuildDir + Invoke-NinjaFallbackBuild -BuildDir $BuildDir +} diff --git a/scripts/test_fx.ps1 b/scripts/test_fx.ps1 new file mode 100644 index 0000000..f60fdae --- /dev/null +++ b/scripts/test_fx.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [ValidateSet('win64', 'win32')] + [string]$Arch = 'win64', + + [AllowNull()] + [object]$Configuration = 'all', + + [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount), + + [int]$RepeatFailCount = 1, + + [int]$TestTimeoutSeconds = 60, + + [switch]$CollectFailureDiagnostics = $true, + + [string]$TestRegex, + + [switch]$RerunFailedOnly, + + [string]$BuildDir, + + [switch]$Clean +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'fx_windows_common.ps1') + +$repoRoot = Split-Path -Parent $PSScriptRoot +$settings = Get-PortSettings -SelectedArch $Arch + +if (-not $BuildDir) { + $BuildDir = Join-Path $repoRoot "build\tests\$Arch" +} + +$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration +Write-Host "Selected configurations: $($selectedConfigurations -join ', ')" + +Enter-VisualStudioDevShell -VsArch $settings.VsArch + +if (($settings.FileXArch -eq 'win32') -or ($settings.FileXArch -eq 'win64')) { + if ($Parallel -ne 1) { + Write-Warning "Windows simulator regression tests are timing-sensitive under concurrent ctest execution. Forcing -Parallel 1." + $Parallel = 1 + } +} + +$failedConfigurations = @() + +foreach ($currentConfiguration in $selectedConfigurations) { + $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration + $currentBuildDir = Join-Path $BuildDir $currentBuildDirName + $currentTestingTemporaryDir = Join-Path $currentBuildDir 'Testing\Temporary' + + try { + if ($Clean) { + $currentTestingDir = Join-Path $currentBuildDir 'Testing' + Remove-CtestTestingDirectory -Path $currentTestingDir + } + + if (-not (Test-Path -LiteralPath $currentBuildDir)) { + throw "Build directory does not exist for $Arch / ${currentConfiguration}: $currentBuildDir" + } + + Remove-NinjaLock -Path $currentBuildDir + if (Test-Path -LiteralPath $currentTestingTemporaryDir) { + Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTest.log') -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTestsFailed.log') -Force -ErrorAction SilentlyContinue + } + + Write-Host "Testing $Arch / $currentConfiguration" + $ctestArguments = @( + '--test-dir', $currentBuildDir, + '--output-on-failure', + '--timeout', $TestTimeoutSeconds.ToString(), + '-j', $Parallel.ToString() + ) + + if ($RepeatFailCount -gt 1) { + $ctestArguments += @('--repeat', "until-pass:$RepeatFailCount") + } + + if ($TestRegex) { + $ctestArguments += @('-R', $TestRegex) + } + + if ($RerunFailedOnly) { + $ctestArguments += '--rerun-failed' + } + + Invoke-NativeCommand -FilePath 'ctest' -Arguments $ctestArguments + } + catch { + if ($CollectFailureDiagnostics -and (Test-Path -LiteralPath $currentBuildDir)) { + try { + Invoke-CtestFailureDiagnostics -BuildDir $currentBuildDir -TestingTemporaryDir $currentTestingTemporaryDir ` + -TimeoutSeconds $TestTimeoutSeconds + } + catch { + Write-Warning "Failure diagnostics collection failed for ${currentConfiguration}: $($_.Exception.Message)" + } + } + + $failedConfigurations += @{ + Configuration = $currentConfiguration + Message = $_.Exception.Message + } + + Write-Warning "Configuration failed: $currentConfiguration" + } +} + +if ($failedConfigurations.Count -gt 0) { + Write-Host '' + Write-Host 'Configuration failure summary:' + foreach ($failedConfiguration in $failedConfigurations) { + Write-Host "- $($failedConfiguration.Configuration): $($failedConfiguration.Message)" + } + + throw "One or more configurations failed: $($failedConfigurations.Configuration -join ', ')" +} diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt index a81cc17..d6ccd66 100644 --- a/test/cmake/CMakeLists.txt +++ b/test/cmake/CMakeLists.txt @@ -2,13 +2,23 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) cmake_policy(SET CMP0054 NEW) cmake_policy(SET CMP0057 NEW) -project(threadx_test LANGUAGES C) +# Suppress try_compile link step when MSVC cannot produce a runnable executable +# (e.g. when the VS environment is not fully initialised for a given target arch). +if((DEFINED THREADX_ARCH) AND ((THREADX_ARCH STREQUAL "win32") OR (THREADX_ARCH STREQUAL "win64"))) + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +endif() + +project(filex_test LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) # Set build configurations set(BUILD_CONFIGURATIONS default_build_coverage no_cache_build no_cache_standalone_build fault_tolerant_build_coverage no_check_build no_cache_fault_tolerant_build - standalone_build_coverage standalone_fault_tolerant_build_coverage + standalone_build_coverage standalone_fault_tolerant_build_coverage standalone_no_cache_fault_tolerant_build) set(CMAKE_CONFIGURATION_TYPES ${BUILD_CONFIGURATIONS} @@ -31,25 +41,32 @@ set(FX_FAULT_TOLERANT_DEFINITIONS set(default_build_coverage "") set(no_cache_build -DFX_DISABLE_CACHE -DFX_DISABLE_FAT_ENTRY_REFRESH) set(fault_tolerant_build_coverage ${FX_FAULT_TOLERANT_DEFINITIONS}) -set(standalone_build_coverage -DFX_STANDALONE_ENABLE) +set(standalone_build_coverage -DFX_STANDALONE_ENABLE) set(standalone_fault_tolerant_build_coverage ${FX_FAULT_TOLERANT_DEFINITIONS} - -DFX_STANDALONE_ENABLE) + -DFX_STANDALONE_ENABLE) set(no_cache_standalone_build -DFX_DISABLE_CACHE -DFX_STANDALONE_ENABLE) -set(no_check_build ${FX_COMPILE_DEFINITIONS} -DFX_DISABLE_ERROR_CHECKING) +set(no_check_build -DFX_DISABLE_ERROR_CHECKING) set(no_cache_fault_tolerant_build ${no_cache_build} ${FX_FAULT_TOLERANT_DEFINITIONS}) set(standalone_no_cache_fault_tolerant_build ${no_cache_build} ${FX_FAULT_TOLERANT_DEFINITIONS} -DFX_STANDALONE_ENABLE) -add_compile_options( - -m32 - -std=c99 - -ggdb - -g3 - -gdwarf-2 - -fdiagnostics-color - -Werror - -DFX_REGRESSION_TEST - ${${CMAKE_BUILD_TYPE}}) -add_link_options(-m32) +if(MSVC) + add_compile_options(/W3 /Zi) + add_link_options(/DEBUG /INCREMENTAL:NO) + # Both cl.exe and gcc accept -DFOO; use it to keep the per-configuration + # definitions consistent with the Linux path. + add_compile_options(-DFX_REGRESSION_TEST ${${CMAKE_BUILD_TYPE}}) +else() + add_compile_options( + -m32 + -ggdb + -g3 + -gdwarf-2 + -fdiagnostics-color + -Werror + -DFX_REGRESSION_TEST + ${${CMAKE_BUILD_TYPE}}) + add_link_options(-m32) +endif() if(CMAKE_BUILD_TYPE MATCHES ".*standalone.*") set(FX_STANDALONE_ENABLE @@ -59,18 +76,30 @@ endif() enable_testing() +# ThreadX dependency: on Windows build from source; on Linux use run.sh. +if(MSVC) + if(NOT FX_STANDALONE_ENABLE) + if(NOT DEFINED THREADX_SOURCE_DIR) + message(FATAL_ERROR + "THREADX_SOURCE_DIR must point to a ThreadX source tree for non-standalone Windows builds. " + "Pass -DTHREADX_SOURCE_DIR= to cmake, or use build_fx.ps1 which sets it automatically.") + endif() + add_subdirectory(${THREADX_SOURCE_DIR} threadx_build) + endif() +endif() + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../.. filex) add_subdirectory(regression) add_subdirectory(samples) -# Coverage -if(CMAKE_BUILD_TYPE MATCHES ".*_coverage") +# Coverage instrumentation (GCC/Clang only — not supported by MSVC) +if(NOT MSVC AND CMAKE_BUILD_TYPE MATCHES ".*_coverage") target_compile_options(filex PRIVATE -fprofile-arcs -ftest-coverage) target_link_options(filex PRIVATE -fprofile-arcs -ftest-coverage) endif() -# Build ThreadX library once -if(NOT FX_STANDALONE_ENABLE) +# Build ThreadX shared library for Linux +if(NOT MSVC AND NOT FX_STANDALONE_ENABLE) execute_process(COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) add_custom_target(build_libs ALL COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) @@ -83,23 +112,27 @@ if(NOT FX_STANDALONE_ENABLE) ${CMAKE_BINARY_DIR}/../libs/threadx/libthreadx.so) endif() -target_compile_options( - filex - PRIVATE -Werror - -Wall - -Wextra - -pedantic - -fmessage-length=0 - -fsigned-char - -ffunction-sections - -fdata-sections - -Wunused - -Wuninitialized - -Wmissing-declarations - -Wconversion - -Wpointer-arith - -Wshadow - -Wlogical-op - -Waggregate-return - -Wfloat-equal) +if(MSVC) + target_compile_options(filex PRIVATE /W3) +else() + target_compile_options( + filex + PRIVATE -Werror + -Wall + -Wextra + -pedantic + -fmessage-length=0 + -fsigned-char + -ffunction-sections + -fdata-sections + -Wunused + -Wuninitialized + -Wmissing-declarations + -Wconversion + -Wpointer-arith + -Wshadow + -Wlogical-op + -Waggregate-return + -Wfloat-equal) +endif() diff --git a/test/cmake/regression/CMakeLists.txt b/test/cmake/regression/CMakeLists.txt index 2674ef6..613f601 100644 --- a/test/cmake/regression/CMakeLists.txt +++ b/test/cmake/regression/CMakeLists.txt @@ -147,6 +147,9 @@ add_library(test_utility ${SOURCE_DIR}/fx_ram_driver_test.c ${SOURCE_DIR}/filextestcontrol.c) target_link_libraries(test_utility PUBLIC azrtos::filex) target_compile_definitions(test_utility PUBLIC BATCH_TEST CTEST) +if(MSVC AND FX_STANDALONE_ENABLE) + target_include_directories(test_utility PUBLIC ${SOURCE_DIR}/win64_compat) +endif() foreach(test_case ${regression_test_cases}) get_filename_component(test_name ${test_case} NAME_WE) @@ -154,3 +157,14 @@ foreach(test_case ${regression_test_cases}) target_link_libraries(${test_name} PRIVATE test_utility) add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name}) endforeach() + +# These two tests write or corrupt large amounts of data on a 900 MB RAM disk and are +# inherently slow (≈95 s and ≈330 s on Windows). Override the default 60-second ctest +# timeout so that they are not mis-reported as failures. +if(MSVC) + set_tests_properties( + ${CMAKE_BUILD_TYPE}::filex_fault_tolerant_file_corruption_test + ${CMAKE_BUILD_TYPE}::filex_fault_tolerant_media_full_test + PROPERTIES TIMEOUT 600 + ) +endif() diff --git a/test/cmake/samples/CMakeLists.txt b/test/cmake/samples/CMakeLists.txt index 17530dc..b9824f0 100644 --- a/test/cmake/samples/CMakeLists.txt +++ b/test/cmake/samples/CMakeLists.txt @@ -12,6 +12,6 @@ foreach(sample_file ${sample_files}) get_filename_component(sample_file_name ${sample_file} NAME_WE) add_executable(${sample_file_name} ${sample_file} ${CMAKE_CURRENT_LIST_DIR}/../../regression_test/fx_ram_driver_test.c) - target_compile_options(${sample_file_name} PRIVATE -DSAMPLE_BUILD) + target_compile_definitions(${sample_file_name} PRIVATE SAMPLE_BUILD) target_link_libraries(${sample_file_name} PRIVATE azrtos::filex) endforeach() diff --git a/test/regression_test/fx_ram_driver_test.c b/test/regression_test/fx_ram_driver_test.c index 63eb4a5..938624f 100644 --- a/test/regression_test/fx_ram_driver_test.c +++ b/test/regression_test/fx_ram_driver_test.c @@ -1,4 +1,4 @@ -/**************************************************************************/ +/**************************************************************************/ /* */ /* Copyright (c) 1996-2017 by Express Logic Inc. */ /* */ @@ -81,6 +81,36 @@ ULONG64 media_size; UCHAR large_file_name_format[]; /* Define memory for tests to be run in standalone mode (without Azure RTOS: ThreadX) */ +/* On MSVC, BSS arrays cause demand-zero page faults on first access, which serialises + with test execution and makes large-disk tests significantly slower than on Linux. + Pre-allocating with calloc commits and zero-fills all pages before any test runs, + eliminating page-fault overhead at runtime. This also avoids LNK1248 (PE image > 2 GB) + for configurations that would otherwise exceed the hard linker limit. + On other compilers keep the BSS arrays as before. */ +#ifdef _MSC_VER +#include +UCHAR *ram_disk_memory_large = NULL; +UCHAR *large_data_buffer = NULL; + +#if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) +UCHAR *ram_disk_memory = NULL; +UCHAR *ram_disk_memory1 = NULL; +#endif + +static void _fx_alloc_test_buffers(void) +{ + ram_disk_memory_large = (UCHAR *)calloc(900000000UL, 1U); + large_data_buffer = (UCHAR *)calloc(900000000UL, 1U); +#if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) + ram_disk_memory = (UCHAR *)calloc(300000000UL, 1U); + ram_disk_memory1 = (UCHAR *)calloc( 30000000UL, 1U); +#endif +} +#pragma section(".CRT$XCU", read) +__declspec(allocate(".CRT$XCU")) static void (*_p_fx_alloc)(void) = _fx_alloc_test_buffers; + +#else /* !_MSC_VER */ + #if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) UCHAR ram_disk_memory[300000000]; UCHAR ram_disk_memory1[30000000]; @@ -88,6 +118,7 @@ UCHAR ram_disk_memory1[30000000]; UCHAR ram_disk_memory_large[900000000]; UCHAR large_data_buffer[900000000]; +#endif /* _MSC_VER */ /* Define the callback function. */ @@ -298,8 +329,16 @@ UINT offset; /* Calculate the RAM disk sector offset. Note the RAM disk memory is pointed to by the fx_media_driver_info pointer, which is supplied by the application in the call to fx_media_open. */ - source_buffer = ((UCHAR *) media_ptr -> fx_media_driver_info) + - ((media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors) * media_ptr -> fx_media_bytes_per_sector); + { + /* Compute effective sector and guard against pointer arithmetic overflow on 64-bit + platforms when sector is near ULONG64_MAX. Out-of-range accesses are redirected + to large_data_buffer so the test can verify return codes without crashing. */ + ULONG64 _sector = media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors; + if (media_ptr -> fx_media_total_sectors > 0 && _sector >= media_ptr -> fx_media_total_sectors) + source_buffer = large_data_buffer; /* harmless scratch — test does not verify data */ + else + source_buffer = ((UCHAR *) media_ptr -> fx_media_driver_info) + (_sector * media_ptr -> fx_media_bytes_per_sector); + } /* Copy the RAM sector into the destination. */ _fx_utility_memory_copy(source_buffer, media_ptr -> fx_media_driver_buffer, @@ -318,8 +357,17 @@ UINT offset; /* Calculate the RAM disk sector offset. Note the RAM disk memory is pointed to by the fx_media_driver_info pointer, which is supplied by the application in the call to fx_media_open. */ - data_start = ((UCHAR *) media_ptr -> fx_media_driver_info) + - ((media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors) * media_ptr -> fx_media_bytes_per_sector); + { + /* Compute effective sector and guard against pointer arithmetic overflow on 64-bit + platforms when sector is near ULONG64_MAX. Out-of-range writes are discarded. */ + ULONG64 _sector = media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors; + if (media_ptr -> fx_media_total_sectors > 0 && _sector >= media_ptr -> fx_media_total_sectors) + { + media_ptr -> fx_media_driver_status = FX_SUCCESS; + return; + } + data_start = ((UCHAR *) media_ptr -> fx_media_driver_info) + (_sector * media_ptr -> fx_media_bytes_per_sector); + } /* Driver write callback function entry for calling. */ if (driver_write_callback != FX_NULL) diff --git a/test/regression_test/win64_compat/pthread.h b/test/regression_test/win64_compat/pthread.h new file mode 100644 index 0000000..2ae24b3 --- /dev/null +++ b/test/regression_test/win64_compat/pthread.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) 2025 Contributors to the Eclipse Foundation */ +/* AI-generated with GitHub Copilot */ + +/* Minimal pthreads shim for FileX regression tests on Windows/x64. */ + +#ifndef WIN64_COMPAT_PTHREAD_H +#define WIN64_COMPAT_PTHREAD_H + +#include + +typedef HANDLE pthread_t; +typedef void pthread_attr_t; + +#define PTHREAD_CANCEL_ENABLE 0 +#define PTHREAD_CANCEL_DISABLE 1 +#define PTHREAD_CANCEL_DEFERRED 0 +#define PTHREAD_CANCEL_ASYNCHRONOUS 1 + +static inline int pthread_create(pthread_t *tid, const pthread_attr_t *attr, + void *(*start_routine)(void *), void *arg) +{ + (void)attr; + *tid = CreateThread(NULL, 0U, (LPTHREAD_START_ROUTINE)(void *)start_routine, arg, 0U, NULL); + return (*tid == NULL) ? -1 : 0; +} + +static inline int pthread_join(pthread_t tid, void **retval) +{ + (void)retval; + WaitForSingleObject(tid, INFINITE); + CloseHandle(tid); + return 0; +} + +static inline int pthread_cancel(pthread_t tid) +{ + TerminateThread(tid, 0U); + return 0; +} + +/* No-op: allows test_control_thread_entry to reach exit(test_control_failed_tests). */ +static inline void pthread_exit(void *retval) +{ + (void)retval; +} + +static inline int pthread_setcancelstate(int state, int *oldstate) +{ + (void)state; + if (oldstate != NULL) + { + *oldstate = PTHREAD_CANCEL_ENABLE; + } + return 0; +} + +static inline int pthread_setcanceltype(int type, int *oldtype) +{ + (void)type; + if (oldtype != NULL) + { + *oldtype = PTHREAD_CANCEL_DEFERRED; + } + return 0; +} + +#endif /* WIN64_COMPAT_PTHREAD_H */ diff --git a/test/regression_test/win64_compat/unistd.h b/test/regression_test/win64_compat/unistd.h new file mode 100644 index 0000000..89a99ee --- /dev/null +++ b/test/regression_test/win64_compat/unistd.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) 2025 Contributors to the Eclipse Foundation */ +/* AI-generated with GitHub Copilot */ + +/* Minimal POSIX unistd shim for FileX regression tests on Windows/x64. */ + +#ifndef WIN64_COMPAT_UNISTD_H +#define WIN64_COMPAT_UNISTD_H + +#include + +typedef unsigned int useconds_t; + +/* Sleep for `usec` microseconds. Windows resolution is ~1 ms; round up. */ +static inline int usleep(useconds_t usec) +{ + DWORD ms = (DWORD)((usec + 999U) / 1000U); + Sleep((ms > 0U) ? ms : 1U); + return 0; +} + +#endif /* WIN64_COMPAT_UNISTD_H */ From 3a17b07bf59af9f81403f6a31a7836a197264157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 28 Jul 2026 11:06:42 -0400 Subject: [PATCH 02/14] Flagged six FileX APIs as deprecated (#95) Added #pragma message compile-time warnings and updated DESCRIPTION blocks in: - fx_directory_long_name_get -> use _extended (buffer size) - fx_directory_short_name_get -> use _extended (buffer size) - fx_media_volume_get -> use _extended (buffer size) - fx_unicode_length_get -> use _extended (buffer size) - fx_unicode_name_get -> use _extended (buffer size) - fx_unicode_short_name_get -> use _extended (buffer size) All six functions omit a destination buffer length parameter and use a fixed or hardcoded limit, which can cause buffer overruns in the caller. The corresponding _extended variants accept an explicit buffer size and must be used instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- common/src/fx_directory_long_name_get.c | 14 +++++++++++++- common/src/fx_directory_short_name_get.c | 14 +++++++++++++- common/src/fx_media_volume_get.c | 14 +++++++++++++- common/src/fx_unicode_length_get.c | 14 +++++++++++++- common/src/fx_unicode_name_get.c | 14 +++++++++++++- common/src/fx_unicode_short_name_get.c | 14 +++++++++++++- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/common/src/fx_directory_long_name_get.c b/common/src/fx_directory_long_name_get.c index d7b397d..c30bc3b 100644 --- a/common/src/fx_directory_long_name_get.c +++ b/common/src/fx_directory_long_name_get.c @@ -29,6 +29,17 @@ #include "fx_directory.h" +/* DEPRECATION NOTICE + * fx_directory_long_name_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. + * + * WHAT TO DO: replace calls with fx_directory_long_name_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_directory_long_name_get() is deprecated. " \ + "Use fx_directory_long_name_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -45,7 +56,8 @@ /* name. If there is no long file name, the short file name will be */ /* returned. */ /* */ -/* Note, this API is deprecated as fx_directory_long_name_get_extended */ +/* DEPRECATED. Use fx_directory_long_name_get_extended() instead, passing the actual + destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. /* should be used. The maximum written size to long_file_name could be */ /* FX_MAX_LONG_NAME_LEN. */ /* */ diff --git a/common/src/fx_directory_short_name_get.c b/common/src/fx_directory_short_name_get.c index a5ec217..ec817a1 100644 --- a/common/src/fx_directory_short_name_get.c +++ b/common/src/fx_directory_short_name_get.c @@ -29,6 +29,17 @@ #include "fx_directory.h" +/* DEPRECATION NOTICE + * fx_directory_short_name_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a destination buffer length; writes up to FX_MAX_SHORT_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. + * + * WHAT TO DO: replace calls with fx_directory_short_name_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_directory_short_name_get() is deprecated. " \ + "Use fx_directory_short_name_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -45,7 +56,8 @@ /* name. If the long file name is really a short file name, the short */ /* file name will be returned. */ /* */ -/* Note, this API is deprecated as fx_directory_short_name_get_extended*/ +/* DEPRECATED. Use fx_directory_short_name_get_extended() instead, passing the actual + destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_SHORT_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. /* should be used. The maximum written size to short_file_name could */ /* be FX_MAX_SHORT_NAME_LEN. */ /* */ diff --git a/common/src/fx_media_volume_get.c b/common/src/fx_media_volume_get.c index 8a9de71..e0e9355 100644 --- a/common/src/fx_media_volume_get.c +++ b/common/src/fx_media_volume_get.c @@ -32,6 +32,17 @@ #include "fx_utility.h" +/* DEPRECATION NOTICE + * fx_media_volume_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a volume-name buffer length; writes up to 12 bytes regardless of the caller's buffer size, risking a buffer overrun. + * + * WHAT TO DO: replace calls with fx_media_volume_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_media_volume_get() is deprecated. " \ + "Use fx_media_volume_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -47,7 +58,8 @@ /* This function reads the volume name stored in the media's boot */ /* record or root directory. */ /* */ -/* Note, this API is deprecated as fx_media_volume_get_extended should */ +/* DEPRECATED. Use fx_media_volume_get_extended() instead, passing the actual + destination buffer length. Does not accept a volume-name buffer length; writes up to 12 bytes regardless of the caller's buffer size, risking a buffer overrun. /* be used. The maximum written size to volume_name could be 12. */ /* */ /* INPUT */ diff --git a/common/src/fx_unicode_length_get.c b/common/src/fx_unicode_length_get.c index 35ee544..3b57c41 100644 --- a/common/src/fx_unicode_length_get.c +++ b/common/src/fx_unicode_length_get.c @@ -29,6 +29,17 @@ #include "fx_unicode.h" +/* DEPRECATION NOTICE + * fx_unicode_length_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a buffer length; scans up to 256 bytes regardless of the actual buffer size, risking an overread. + * + * WHAT TO DO: replace calls with fx_unicode_length_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_unicode_length_get() is deprecated. " \ + "Use fx_unicode_length_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -43,7 +54,8 @@ /* */ /* This function returns the length of the supplied unicode name. */ /* */ -/* Note, this API is deprecated as _fx_unicode_length_get_extended */ +/* DEPRECATED. Use fx_unicode_length_get_extended() instead, passing the actual + destination buffer length. Does not accept a buffer length; scans up to 256 bytes regardless of the actual buffer size, risking an overread. /* should be used. The maximum buffer size of unicode_name is 256. */ /* */ /* INPUT */ diff --git a/common/src/fx_unicode_name_get.c b/common/src/fx_unicode_name_get.c index 50435af..3284513 100644 --- a/common/src/fx_unicode_name_get.c +++ b/common/src/fx_unicode_name_get.c @@ -29,6 +29,17 @@ #include "fx_unicode.h" +/* DEPRECATION NOTICE + * fx_unicode_name_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN*2 bytes regardless of the caller's buffer size, risking a buffer overrun. + * + * WHAT TO DO: replace calls with fx_unicode_name_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_unicode_name_get() is deprecated. " \ + "Use fx_unicode_name_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -44,7 +55,8 @@ /* This function finds the unicode name associated with the supplied */ /* short 8.3 name. */ /* */ -/* Note, this API is deprecated as fx_unicode_name_get_extended should */ +/* DEPRECATED. Use fx_unicode_name_get_extended() instead, passing the actual + destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN*2 bytes regardless of the caller's buffer size, risking a buffer overrun. /* be used. The maximum written size to destination_unicode_name could */ /* be FX_MAX_LONG_NAME_LEN * 2. */ /* */ diff --git a/common/src/fx_unicode_short_name_get.c b/common/src/fx_unicode_short_name_get.c index b0a6fa3..c177c1b 100644 --- a/common/src/fx_unicode_short_name_get.c +++ b/common/src/fx_unicode_short_name_get.c @@ -29,6 +29,17 @@ #include "fx_unicode.h" +/* DEPRECATION NOTICE + * fx_unicode_short_name_get() is deprecated. Do not use it in new code. + * + * WHY: Does not accept a destination buffer length; writes up to 13 bytes regardless of the caller's buffer size, risking a buffer overrun. + * + * WHAT TO DO: replace calls with fx_unicode_short_name_get_extended(), passing the actual + * destination buffer size as an additional argument. + */ +#pragma message("fx_unicode_short_name_get() is deprecated. " \ + "Use fx_unicode_short_name_get_extended() and pass the actual buffer size.") + /**************************************************************************/ /* */ /* FUNCTION RELEASE */ @@ -44,7 +55,8 @@ /* This function finds the short name associated with the supplied */ /* unicode name. */ /* */ -/* Note, this API is deprecated as fx_unicode_short_name_get_extended */ +/* DEPRECATED. Use fx_unicode_short_name_get_extended() instead, passing the actual + destination buffer length. Does not accept a destination buffer length; writes up to 13 bytes regardless of the caller's buffer size, risking a buffer overrun. /* should be used. The maximum written size to destination_short_name */ /* could be 13. */ /* */ From cd015e5e82586fef9b9a4428b0bbd39a8f441d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 16 Sep 2026 10:16:45 -0400 Subject: [PATCH 03/14] Restored the comment boxes around the deprecation notes (#101) The deprecation notes added in #95 were pasted into the function header blocks without the surrounding box. Two of the six left a bare /* on the following line, which GCC rejects as "/*" within comment under -Werror, so the library no longer compiled and no regression test could be built. Each note is rewrapped inside the box at the width the rest of the header uses. The orphaned tail of the sentence the paste replaced is gone too: it restated the maximum written size that the new wording already gives. The library builds again and 136/136 pass in standalone_no_cache_fault_tolerant_build, against no build at all before. Assisted-by: Claude Code (Opus 5) --- common/src/fx_directory_long_name_get.c | 10 ++++++---- common/src/fx_directory_short_name_get.c | 10 ++++++---- common/src/fx_media_volume_get.c | 9 ++++++--- common/src/fx_unicode_length_get.c | 9 ++++++--- common/src/fx_unicode_name_get.c | 10 ++++++---- common/src/fx_unicode_short_name_get.c | 10 ++++++---- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/common/src/fx_directory_long_name_get.c b/common/src/fx_directory_long_name_get.c index c30bc3b..dfd6fac 100644 --- a/common/src/fx_directory_long_name_get.c +++ b/common/src/fx_directory_long_name_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -56,10 +58,10 @@ /* name. If there is no long file name, the short file name will be */ /* returned. */ /* */ -/* DEPRECATED. Use fx_directory_long_name_get_extended() instead, passing the actual - destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. -/* should be used. The maximum written size to long_file_name could be */ -/* FX_MAX_LONG_NAME_LEN. */ +/* DEPRECATED. Use fx_directory_long_name_get_extended() instead, */ +/* passing the actual destination buffer length. It takes no buffer */ +/* length and writes up to FX_MAX_LONG_NAME_LEN bytes whatever the size*/ +/* of the caller's buffer, risking an overrun. */ /* */ /* INPUT */ /* */ diff --git a/common/src/fx_directory_short_name_get.c b/common/src/fx_directory_short_name_get.c index ec817a1..e3fbe46 100644 --- a/common/src/fx_directory_short_name_get.c +++ b/common/src/fx_directory_short_name_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -56,10 +58,10 @@ /* name. If the long file name is really a short file name, the short */ /* file name will be returned. */ /* */ -/* DEPRECATED. Use fx_directory_short_name_get_extended() instead, passing the actual - destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_SHORT_NAME_LEN bytes regardless of the caller's buffer size, risking a buffer overrun. -/* should be used. The maximum written size to short_file_name could */ -/* be FX_MAX_SHORT_NAME_LEN. */ +/* DEPRECATED. Use fx_directory_short_name_get_extended() instead, */ +/* passing the actual destination buffer length. This function takes no*/ +/* buffer length and writes up to FX_MAX_SHORT_NAME_LEN bytes whatever */ +/* the size of the caller's buffer, risking an overrun. */ /* */ /* INPUT */ /* */ diff --git a/common/src/fx_media_volume_get.c b/common/src/fx_media_volume_get.c index e0e9355..de15326 100644 --- a/common/src/fx_media_volume_get.c +++ b/common/src/fx_media_volume_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -58,9 +60,10 @@ /* This function reads the volume name stored in the media's boot */ /* record or root directory. */ /* */ -/* DEPRECATED. Use fx_media_volume_get_extended() instead, passing the actual - destination buffer length. Does not accept a volume-name buffer length; writes up to 12 bytes regardless of the caller's buffer size, risking a buffer overrun. -/* be used. The maximum written size to volume_name could be 12. */ +/* DEPRECATED. Use fx_media_volume_get_extended() instead, passing the */ +/* actual destination buffer length. This function takes no buffer */ +/* length and writes up to 12 bytes whatever the size of the caller's */ +/* buffer, risking an overrun. */ /* */ /* INPUT */ /* */ diff --git a/common/src/fx_unicode_length_get.c b/common/src/fx_unicode_length_get.c index 3b57c41..971c4b8 100644 --- a/common/src/fx_unicode_length_get.c +++ b/common/src/fx_unicode_length_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -54,9 +56,10 @@ /* */ /* This function returns the length of the supplied unicode name. */ /* */ -/* DEPRECATED. Use fx_unicode_length_get_extended() instead, passing the actual - destination buffer length. Does not accept a buffer length; scans up to 256 bytes regardless of the actual buffer size, risking an overread. -/* should be used. The maximum buffer size of unicode_name is 256. */ +/* DEPRECATED. Use fx_unicode_length_get_extended() instead, passing */ +/* the actual buffer length. It takes no buffer length and scans */ +/* up to 256 bytes whatever the size of the actual buffer, risking an */ +/* over-read. */ /* */ /* INPUT */ /* */ diff --git a/common/src/fx_unicode_name_get.c b/common/src/fx_unicode_name_get.c index 3284513..cc9b6f3 100644 --- a/common/src/fx_unicode_name_get.c +++ b/common/src/fx_unicode_name_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -55,10 +57,10 @@ /* This function finds the unicode name associated with the supplied */ /* short 8.3 name. */ /* */ -/* DEPRECATED. Use fx_unicode_name_get_extended() instead, passing the actual - destination buffer length. Does not accept a destination buffer length; writes up to FX_MAX_LONG_NAME_LEN*2 bytes regardless of the caller's buffer size, risking a buffer overrun. -/* be used. The maximum written size to destination_unicode_name could */ -/* be FX_MAX_LONG_NAME_LEN * 2. */ +/* DEPRECATED. Use fx_unicode_name_get_extended() instead, passing the */ +/* actual destination buffer length. This function takes no buffer */ +/* length and writes up to FX_MAX_LONG_NAME_LEN * 2 bytes whatever the */ +/* size of the caller's buffer, risking an overrun. */ /* */ /* INPUT */ /* */ diff --git a/common/src/fx_unicode_short_name_get.c b/common/src/fx_unicode_short_name_get.c index c177c1b..927f9f3 100644 --- a/common/src/fx_unicode_short_name_get.c +++ b/common/src/fx_unicode_short_name_get.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -55,10 +57,10 @@ /* This function finds the short name associated with the supplied */ /* unicode name. */ /* */ -/* DEPRECATED. Use fx_unicode_short_name_get_extended() instead, passing the actual - destination buffer length. Does not accept a destination buffer length; writes up to 13 bytes regardless of the caller's buffer size, risking a buffer overrun. -/* should be used. The maximum written size to destination_short_name */ -/* could be 13. */ +/* DEPRECATED. Use fx_unicode_short_name_get_extended() instead, */ +/* passing the actual destination buffer length. It takes no buffer */ +/* length and writes up to 13 bytes whatever the size of the caller's */ +/* buffer, risking an overrun. */ /* */ /* INPUT */ /* */ From 408d1f220f4a833e139cebd5a78d6980f0cf491b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 07:28:49 -0400 Subject: [PATCH 04/14] Stopped the release script adding a Co-authored-by trailer (#108) prepare_release.sh committed the version passes with a Co-authored-by trailer naming an AI. That trailer asserts authorship an AI cannot hold: the human contributor signs the ECA and is solely responsible for the contribution. It is already in the published history, on the 6.5.1.202602 preparation. The commits now carry their subject alone. A version pass is mechanical sed output, so no agent produces it at run time; an agent that runs the script records its own Assisted-by trailer on that run instead. The script also gains the AI disclosure line it was missing. Ran the patched script against a scratch clone targeting 6.5.2.202603. Both commits come out with an empty trailer block, and the version constants and three port version strings update as before. Assisted-by: Claude Code (Opus 5) --- scripts/prepare_release.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index f9b60c5..0124b47 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -15,6 +15,7 @@ # # Copyright (C) 2026 Eclipse ThreadX contributors # SPDX-License-Identifier: MIT +# Portions of this file were generated with AI assistance. set -eu @@ -120,8 +121,6 @@ sed -i -E "s|(#define FILEX_HOTFIX_VERSION[[:space:]]+)'[^']*'|\1${HOTFIX_DEFINE git -C "${REPO_ROOT}" add "${API_HEADER}" git -C "${REPO_ROOT}" commit -F - <<'COMMIT_EOF' Updated version number constants - -Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> COMMIT_EOF printf "Committed version constant updates.\n" @@ -161,8 +160,6 @@ EOF else git -C "${REPO_ROOT}" commit -F - <<'COMMIT_EOF' Updated port version strings - -Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> COMMIT_EOF printf "Committed port version string updates.\n" fi From 42eb62c188e730f1ebb6d63cf6170389cb72547c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 16:42:17 -0400 Subject: [PATCH 05/14] Merge commit from fork * fix: harden fault-tolerant log replay bounds Prevent attacker-controlled FAT log records from causing memory corruption during fault-tolerant recovery. - Bound directory-log copies to fx_media_bytes_per_sector, the actual destination sector size, instead of the whole cache allocation. - Use 64-bit offset-plus-length arithmetic to prevent wraparound. - Reject undersized FAT and directory log entries before typed access or payload-length subtraction. - Validate total_size against the structural minimum, configured maximum, and allocated fault-tolerant buffer before checksum processing. This prevents the replay-path OOB write and the enable-time OOB read caused by malformed on-disk fault-tolerant logs. Assisted By: GPT 5.6 * Corrected the fault-tolerant log bounds and kept clean logs mountable The replay bound rejected a log file that a reset had just written. A clean log records a total size of the header plus the FAT chain, and the enable path demanded one header more than that, so every re-mount of a media carrying an intact log returned FX_FILE_CORRUPT. The replay loop also measured the space left for log entries from the start of the log file rather than from the first entry, leaving the cursor able to run one content offset past the buffer. The minimum is now the size a reset writes, and the entry budget excludes the header, the FAT chain and the content header it sits behind. Neither change touches the destination bound that closes the out-of-bounds write. A new regression test stages a directory log whose copy lands past the destination sector, checks that a copy ending on the sector boundary is still accepted, and enables fault tolerance twice across a close and a re-open. 137/137 fault-tolerant tests pass; the new test fails on both defects independently when either is reverted. Assisted-by: Claude Code (Opus 5) * Extended the fault-tolerant log bounds test to every guard The test shipped with this fix proved one of the four bounds the fix adds. Reverting fx_fault_tolerant_enable in its entirety, or either of the two remaining guards in fx_fault_tolerant_apply_logs, left the suite green. Three cases cover the rest. One shrinks the recorded log size so an entry claims more than the file holds, which is the accounting the old bound got wrong by the width of the header and the FAT chain. One declares a FAT log entry shorter than an entry, desynchronising the walk; a directory entry was not used because the copy bound catches that case one step on and returns the same status. One rewrites the log header on the media to record a full-size log and then enables fault tolerance over a two-sector buffer, which the interface accepts. Locating the header needed a second helper. log_data_sector names a sector one cluster past the header, so reusing it wrote to the wrong place and the case passed for the wrong reason. Reverting each of the four guards on its own now fails the test, as does reverting either source file whole. Nine configurations, 137 tests each, zero warnings, zero errors. Assisted-by: Claude Code (Opus 5) --------- Co-authored-by: Haithem Rahmani --- common/src/fx_fault_tolerant_apply_logs.c | 21 +- common/src/fx_fault_tolerant_enable.c | 14 + test/cmake/regression/CMakeLists.txt | 1 + ...ex_fault_tolerant_apply_logs_bounds_test.c | 422 ++++++++++++++++++ 4 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c diff --git a/common/src/fx_fault_tolerant_apply_logs.c b/common/src/fx_fault_tolerant_apply_logs.c index 75fb578..628b189 100644 --- a/common/src/fx_fault_tolerant_apply_logs.c +++ b/common/src/fx_fault_tolerant_apply_logs.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -101,7 +103,16 @@ FX_FAULT_TOLERANT_DIR_LOG *dir_log; /* Extended port-specific processing macro, which is by default defined to white space. */ FX_FAULT_TOLERANT_APPLY_LOGS_EXTENSION - size -= FX_FAULT_TOLERANT_LOG_CONTENT_HEADER_SIZE; + /* The log entries start after the header, the FAT chain and the content header, + so only the bytes past all three are available to them. */ + if (size < (FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET + FX_FAULT_TOLERANT_LOG_CONTENT_HEADER_SIZE)) + { + + /* Something wrong with log file. */ + return(FX_FILE_CORRUPT); + } + + size -= (FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET + FX_FAULT_TOLERANT_LOG_CONTENT_HEADER_SIZE); /* Find the number of log entries. */ remaining_logs = _fx_utility_16_unsigned_read((UCHAR *)&log_content -> fx_fault_tolerant_log_content_count); @@ -121,7 +132,11 @@ FX_FAULT_TOLERANT_DIR_LOG *dir_log; /* Validate log entry size. */ - if (log_len > size) + if (((log_type == FX_FAULT_TOLERANT_FAT_LOG_TYPE) && + (log_len < FX_FAULT_TOLERANT_FAT_LOG_ENTRY_SIZE)) || + ((log_type == FX_FAULT_TOLERANT_DIR_LOG_TYPE) && + (log_len < FX_FAULT_TOLERANT_DIR_LOG_ENTRY_SIZE)) || + (log_len > size)) { /* Something wrong with log file. */ @@ -178,7 +193,7 @@ FX_FAULT_TOLERANT_DIR_LOG *dir_log; copy_size = log_len - FX_FAULT_TOLERANT_DIR_LOG_ENTRY_SIZE; - if (((ULONG64)copy_offset + (ULONG64)copy_size) > (ULONG64)(media_ptr -> fx_media_memory_size)) + if (((ULONG64)copy_offset + (ULONG64)copy_size) > (ULONG64)(media_ptr -> fx_media_bytes_per_sector)) { return(FX_FILE_CORRUPT); } diff --git a/common/src/fx_fault_tolerant_enable.c b/common/src/fx_fault_tolerant_enable.c index 21b4d1e..9c436f2 100644 --- a/common/src/fx_fault_tolerant_enable.c +++ b/common/src/fx_fault_tolerant_enable.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Portions of this file were generated with AI assistance. + /**************************************************************************/ /**************************************************************************/ @@ -368,6 +370,18 @@ ULONG bytes_per_cluster; /* Fault tolerant log file is valid. */ /* Initialize file size. */ total_size = _fx_utility_16_unsigned_read((UCHAR *)&log_header -> fx_fault_tolerant_log_header_total_size); + + /* A reset log file carries just the header and the FAT chain, so that + is the smallest size on record. The log must also fit both the format + maximum and the buffer the caller supplied. */ + if ((total_size < FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET) || + (total_size > FX_FAULT_TOLERANT_MAXIMUM_LOG_FILE_SIZE) || + (total_size > media_ptr -> fx_media_fault_tolerant_memory_buffer_size)) + { + FX_UNPROTECT + return(FX_FILE_CORRUPT); + } + media_ptr -> fx_media_fault_tolerant_file_size = total_size; diff --git a/test/cmake/regression/CMakeLists.txt b/test/cmake/regression/CMakeLists.txt index 613f601..c806e5d 100644 --- a/test/cmake/regression/CMakeLists.txt +++ b/test/cmake/regression/CMakeLists.txt @@ -15,6 +15,7 @@ set(regression_test_cases ${SOURCE_DIR}/filex_directory_long_short_get_test.c ${SOURCE_DIR}/filex_directory_naming_test.c ${SOURCE_DIR}/filex_directory_rename_test.c + ${SOURCE_DIR}/filex_fault_tolerant_apply_logs_bounds_test.c ${SOURCE_DIR}/filex_fault_tolerant_corrupted_log_file_test.c ${SOURCE_DIR}/filex_fault_tolerant_delete_large_data_test.c ${SOURCE_DIR}/filex_fault_tolerant_directory_attributes_set_directory_interrupt_test.c diff --git a/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c b/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c new file mode 100644 index 0000000..0b622a8 --- /dev/null +++ b/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c @@ -0,0 +1,422 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/* This FileX test concentrates on the bounds applied to fault-tolerant log replay. + + A directory log entry names the offset and the size of a copy into a single + sector staged at fx_media_memory_buffer. Both values come off the media, so + the replay path has to bound them against the sector size rather than against + the whole cache allocation, and a re-mounted clean log has to keep working. */ + +#ifndef FX_STANDALONE_ENABLE +#include "tx_api.h" +#include "tx_thread.h" +#include "tx_timer.h" +#endif +#include "fx_api.h" +#include "fx_utility.h" +#include "fx_fault_tolerant.h" +#include +#include "fx_ram_driver_test.h" +extern void test_control_return(UINT status); +void filex_fault_tolerant_apply_logs_bounds_test_application_define(void *first_unused_memory); + +#if defined (FX_ENABLE_FAULT_TOLERANT) && defined (FX_FAULT_TOLERANT) + +#define DEMO_STACK_SIZE 4096 +#define CACHE_SIZE 2048 +#define FAULT_TOLERANT_SIZE FX_FAULT_TOLERANT_MINIMAL_BUFFER_SIZE +#define BYTES_PER_SECTOR 256 + + +/* Define the ThreadX and FileX object control blocks... */ + +#ifndef FX_STANDALONE_ENABLE +static TX_THREAD ftest_0; +static UCHAR *cache_buffer; +static UCHAR *fault_tolerant_buffer; +#else +static UCHAR cache_buffer[CACHE_SIZE]; +static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; +#endif +static FX_MEDIA ram_disk; +static UCHAR *pointer; + +/* Define thread prototypes. */ + +static void ftest_0_entry(ULONG thread_input); +extern void _fx_ram_driver(FX_MEDIA *media_ptr); +extern void test_control_return(UINT status); + + +/* Format and open the media, then enable fault tolerance. */ +static UINT media_prepare(VOID) +{ + +UINT status; + + status = fx_media_format(&ram_disk, + _fx_ram_driver, // Driver entry + ram_disk_memory_large, // RAM disk memory pointer + cache_buffer, // Media buffer pointer + CACHE_SIZE, // Media buffer size + "MY_RAM_DISK", // Volume Name + 1, // Number of FATs + 32, // Directory Entries + 0, // Hidden sectors + 256, // Total sectors + BYTES_PER_SECTOR, // Sector size + 8, // Sectors per cluster + 1, // Heads + 1); // Sectors per track + if (status != FX_SUCCESS) + { + return(status); + } + + status = fx_media_open(&ram_disk, "RAM DISK", _fx_ram_driver, ram_disk_memory_large, + cache_buffer, CACHE_SIZE); + if (status != FX_SUCCESS) + { + return(status); + } + + return(fx_fault_tolerant_enable(&ram_disk, fault_tolerant_buffer, FAULT_TOLERANT_SIZE)); +} + + +/* Return a logical sector inside the fault-tolerant log file, past its header. */ +static ULONG64 log_data_sector(VOID) +{ + + return(((ULONG64)ram_disk.fx_media_data_sector_start) + + (((ULONG64)(ram_disk.fx_media_fault_tolerant_start_cluster + 1 - FX_FAT_ENTRY_START)) * + ((ULONG64)ram_disk.fx_media_sectors_per_cluster))); +} + + +/* Return the logical sector holding the fault-tolerant log file's header. */ +static ULONG64 log_header_sector(VOID) +{ + + return(((ULONG64)ram_disk.fx_media_data_sector_start) + + (((ULONG64)(ram_disk.fx_media_fault_tolerant_start_cluster - FX_FAT_ENTRY_START)) * + ((ULONG64)ram_disk.fx_media_sectors_per_cluster))); +} + + +/* Stage one directory log entry and force its copy offset, then replay the log. */ +static UINT replay_dir_log_at_offset(ULONG offset) +{ + +UINT status; +UINT payload = 0; +FX_FAULT_TOLERANT_DIR_LOG *dir_log; + + status = media_prepare(); + if (status != FX_SUCCESS) + { + return(status); + } + + _fx_fault_tolerant_transaction_start(&ram_disk); + + /* The next log entry lands at the current end of the log file. */ + dir_log = (FX_FAULT_TOLERANT_DIR_LOG *)(ram_disk.fx_media_fault_tolerant_memory_buffer + + ram_disk.fx_media_fault_tolerant_file_size); + + status = _fx_fault_tolerant_add_dir_log(&ram_disk, log_data_sector(), 1, (UCHAR *)&payload, 1); + if (status != FX_SUCCESS) + { + return(status); + } + + /* Point the copy past the end of the destination sector. */ + _fx_utility_32_unsigned_write((UCHAR *)&dir_log -> fx_fault_tolerant_dir_log_offset, offset); + + return(_fx_fault_tolerant_transaction_end(&ram_disk)); +} + + +/* Stage one directory log entry, then shrink the recorded log size so the entry + claims more than the log file holds, and replay the log. */ +static UINT replay_dir_log_short_by(ULONG shortfall) +{ + +UINT status; +UINT payload = 0; + + status = media_prepare(); + if (status != FX_SUCCESS) + { + return(status); + } + + _fx_fault_tolerant_transaction_start(&ram_disk); + + status = _fx_fault_tolerant_add_dir_log(&ram_disk, log_data_sector(), 1, (UCHAR *)&payload, 1); + if (status != FX_SUCCESS) + { + return(status); + } + + ram_disk.fx_media_fault_tolerant_file_size -= shortfall; + + return(_fx_fault_tolerant_transaction_end(&ram_disk)); +} + + +/* Stage one FAT log entry, then declare it shorter than a FAT log entry, and + replay the log. */ +static UINT replay_short_fat_log(VOID) +{ + +UINT status; +FX_FAULT_TOLERANT_FAT_LOG *fat_log; + + status = media_prepare(); + if (status != FX_SUCCESS) + { + return(status); + } + + _fx_fault_tolerant_transaction_start(&ram_disk); + + /* The next log entry lands at the current end of the log file. */ + fat_log = (FX_FAULT_TOLERANT_FAT_LOG *)(ram_disk.fx_media_fault_tolerant_memory_buffer + + ram_disk.fx_media_fault_tolerant_file_size); + + status = _fx_fault_tolerant_add_FAT_log(&ram_disk, ram_disk.fx_media_fault_tolerant_start_cluster, + ram_disk.fx_media_fat_last); + if (status != FX_SUCCESS) + { + return(status); + } + + /* Declare one word less than the entry occupies. */ + _fx_utility_16_unsigned_write((UCHAR *)&fat_log -> fx_fault_tolerant_FAT_log_size, + FX_FAULT_TOLERANT_FAT_LOG_ENTRY_SIZE - 4); + + return(_fx_fault_tolerant_transaction_end(&ram_disk)); +} + + +/* Rewrite the log file header on the media so it records total_size, keeping the + header checksum valid, then enable fault tolerance over a buffer of + buffer_size bytes. */ +static UINT enable_over_log_recording(USHORT total_size, UINT buffer_size) +{ + +UINT status; +USHORT checksum; +ULONG64 header_sector; +FX_FAULT_TOLERANT_LOG_HEADER *log_header; + + status = media_prepare(); + if (status != FX_SUCCESS) + { + return(status); + } + + /* The log file's location is only known while fault tolerance is enabled. */ + header_sector = log_header_sector(); + + status = fx_media_close(&ram_disk); + if (status != FX_SUCCESS) + { + return(status); + } + + status = fx_media_open(&ram_disk, "RAM DISK", _fx_ram_driver, ram_disk_memory_large, + cache_buffer, CACHE_SIZE); + if (status != FX_SUCCESS) + { + return(status); + } + + /* Read the first sector of the log file, which carries the header. */ + status = _fx_utility_logical_sector_read(&ram_disk, header_sector, + ram_disk.fx_media_memory_buffer, + ((ULONG) 1), FX_DATA_SECTOR); + if (status != FX_SUCCESS) + { + return(status); + } + + log_header = (FX_FAULT_TOLERANT_LOG_HEADER *)ram_disk.fx_media_memory_buffer; + + /* The header checksum covers its own field, so zero it before computing. */ + _fx_utility_16_unsigned_write((UCHAR *)&log_header -> fx_fault_tolerant_log_header_total_size, total_size); + _fx_utility_16_unsigned_write((UCHAR *)&log_header -> fx_fault_tolerant_log_header_checksum, 0); + checksum = _fx_fault_tolerant_calculate_checksum((UCHAR *)log_header, FX_FAULT_TOLERANT_LOG_HEADER_SIZE); + _fx_utility_16_unsigned_write((UCHAR *)&log_header -> fx_fault_tolerant_log_header_checksum, checksum); + + status = _fx_utility_logical_sector_write(&ram_disk, header_sector, + ram_disk.fx_media_memory_buffer, + ((ULONG) 1), FX_DIRECTORY_SECTOR); + if (status != FX_SUCCESS) + { + return(status); + } + + status = _fx_utility_logical_sector_flush(&ram_disk, ((ULONG64) 0), ram_disk.fx_media_total_sectors, FX_TRUE); + if (status != FX_SUCCESS) + { + return(status); + } + + return(fx_fault_tolerant_enable(&ram_disk, fault_tolerant_buffer, buffer_size)); +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void filex_fault_tolerant_apply_logs_bounds_test_application_define(void *first_unused_memory) +#endif +{ + + +#ifndef FX_STANDALONE_ENABLE + /* Setup the working pointer. */ + pointer = (UCHAR *) first_unused_memory; + + /* Create the main thread. */ + + tx_thread_create(&ftest_0, "thread 0", ftest_0_entry, 0, + pointer, DEMO_STACK_SIZE, + 4, 4, TX_NO_TIME_SLICE, TX_AUTO_START); + + pointer = pointer + DEMO_STACK_SIZE; + + /* Setup memory for the RAM disk and the sector cache. */ + cache_buffer = pointer; + pointer += CACHE_SIZE; + fault_tolerant_buffer = pointer; + pointer += FAULT_TOLERANT_SIZE; +#endif + + /* Initialize the FileX system. */ + fx_system_initialize(); +#ifdef FX_STANDALONE_ENABLE + ftest_0_entry(0); +#endif +} + + +/* Define the test threads. */ + +static void ftest_0_entry(ULONG thread_input) +{ + +UINT status; +USHORT total_size; + + FX_PARAMETER_NOT_USED(thread_input); + + /* Print out some test information banners. */ + printf("FileX Test: Fault Tolerant Apply Logs Bounds test.................."); + + /* A copy that runs past the destination sector must be refused. The sector + staged at fx_media_memory_buffer is one sector wide, so an offset inside the + cache allocation but past the sector is the case that used to be accepted. */ + status = replay_dir_log_at_offset(BYTES_PER_SECTOR + 44); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A copy that ends exactly on the sector boundary is still legal. */ + status = replay_dir_log_at_offset(BYTES_PER_SECTOR - 1); + return_if_fail( status == FX_SUCCESS); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A clean log file records no content beyond the header and the FAT chain, and + enabling fault tolerance over one on a re-mounted media has to succeed. */ + status = media_prepare(); + return_if_fail( status == FX_SUCCESS); + + total_size = (USHORT)_fx_utility_16_unsigned_read(ram_disk.fx_media_fault_tolerant_memory_buffer + + sizeof(ULONG)); + return_if_fail( total_size == FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + status = fx_media_open(&ram_disk, "RAM DISK", _fx_ram_driver, ram_disk_memory_large, + cache_buffer, CACHE_SIZE); + return_if_fail( status == FX_SUCCESS); + + status = fx_fault_tolerant_enable(&ram_disk, fault_tolerant_buffer, FAULT_TOLERANT_SIZE); + return_if_fail( status == FX_SUCCESS); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A log entry may claim only the bytes that follow the header, the FAT chain + and the content header. The bound was computed from the content header + alone, which left every entry free to claim the 36 bytes in front of it. */ + status = replay_dir_log_short_by(1); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A FAT log entry that declares less than one entry desynchronises the walk: + the entry is consumed, the cursor advances by less than the entry occupies, + and every entry behind it is then read from the middle of this one. */ + status = replay_short_fat_log(); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A log file recording more bytes than the caller's buffer holds has to be + refused. fx_fault_tolerant_enable accepts any buffer of at least one + sector, so a small buffer under a full-size log is reachable through the + public API, and everything past this point indexes the buffer by the + recorded size. */ + status = enable_over_log_recording(FX_FAULT_TOLERANT_MAXIMUM_LOG_FILE_SIZE, BYTES_PER_SECTOR * 2); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* Output successful. */ + printf("SUCCESS!\n"); + test_control_return(0); +} + +#else + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void filex_fault_tolerant_apply_logs_bounds_test_application_define(void *first_unused_memory) +#endif +{ + + FX_PARAMETER_NOT_USED(first_unused_memory); + + /* Print out some test information banners. */ + printf("FileX Test: Fault Tolerant Apply Logs Bounds test..................N/A\n"); + + test_control_return(255); +} +#endif From a206e7adc83d7649c3760a056b9f72cf0829f199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:00:06 -0400 Subject: [PATCH 06/14] Completed FileX Windows simulator regression support (#100) Improved FileX Win32 build and regression support Completed and hardened the Win32 development and regression-test setup for FileX. - Added missing CMake support for the VS 2019 Win32 port - Corrected the Win32 FileX version information - Fixed regression-build issues and crashes with large media - Corrected buffer declarations for standalone and fault-tolerant configurations - Enabled large-address-aware Windows test executables - Shortened regression target paths to avoid Windows/MSVC path issues - Improved ThreadX path defaults and validation in the Windows build script - Added and standardized the required AI disclosure notices Assisted-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Codex (gpt-5.6-sol) Assisted-by: Claude Code (Opus 5) --- ports/win32/vs_2019/CMakeLists.txt | 12 ++ ports/win32/vs_2019/inc/fx_port.h | 197 ++++++++++++++++-- scripts/build_fx.ps1 | 8 +- test/cmake/CMakeLists.txt | 10 +- test/cmake/regression/CMakeLists.txt | 18 +- .../filex_directory_create_delete_test.c | 5 +- .../filex_directory_duplicate_entries_test.c | 4 + .../filex_directory_rename_test.c | 4 + .../filex_file_create_delete_test.c | 4 + test/regression_test/filex_file_rename_test.c | 4 + test/regression_test/fx_ram_driver_test.h | 15 ++ 11 files changed, 258 insertions(+), 23 deletions(-) create mode 100644 ports/win32/vs_2019/CMakeLists.txt diff --git a/ports/win32/vs_2019/CMakeLists.txt b/ports/win32/vs_2019/CMakeLists.txt new file mode 100644 index 0000000..d5b601d --- /dev/null +++ b/ports/win32/vs_2019/CMakeLists.txt @@ -0,0 +1,12 @@ +# Portions of this file were generated with AI assistance. + +target_sources(${PROJECT_NAME} PRIVATE + # {{BEGIN_TARGET_SOURCES}} + + # {{END_TARGET_SOURCES}} +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/inc +) diff --git a/ports/win32/vs_2019/inc/fx_port.h b/ports/win32/vs_2019/inc/fx_port.h index 3fa5d2a..d0288df 100644 --- a/ports/win32/vs_2019/inc/fx_port.h +++ b/ports/win32/vs_2019/inc/fx_port.h @@ -1,14 +1,19 @@ /*************************************************************************** - * Copyright (c) 2024 Microsoft Corporation - * Copyright (c) 2026-present Eclipse ThreadX contributors + * Copyright (C) 2026 Eclipse ThreadX contributors * * This program and the accompanying materials are made available under the * terms of the MIT License which is available at * https://opensource.org/licenses/MIT. * - * SPDX-License-Identifier: MIT + * AI Disclosure: This file was largely AI-generated by Copilot (Claude Opus 4.8). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 **************************************************************************/ +// Portions of this file were generated with AI assistance. /**************************************************************************/ /**************************************************************************/ @@ -26,11 +31,11 @@ /* PORT SPECIFIC C INFORMATION RELEASE */ /* */ /* fx_port.h Win32/Visual */ -/* 6.3.0 */ +/* 6.5.1.202602 */ /* */ /* AUTHOR */ /* */ -/* William E. Lamie, Microsoft Corporation */ +/* Eclipse ThreadX contributors */ /* */ /* DESCRIPTION */ /* */ @@ -57,6 +62,8 @@ #include "fx_user.h" #endif +#include + /* Include the ThreadX api file. */ @@ -98,7 +105,8 @@ typedef unsigned long long ULONG64; /* Define basic alignment type used in block and byte pool operations. This data type must - be at least 32-bits in size and also be large enough to hold a pointer type. */ + be at least 32-bits in size and also be large enough to hold a pointer type. + On Win32 (ILP32) ULONG is 32 bits and holds a pointer, so ULONG is sufficient. */ #ifndef ALIGN_TYPE_DEFINED #define ALIGN_TYPE_DEFINED @@ -106,19 +114,173 @@ typedef unsigned long long ULONG64; #endif -#endif +#endif /* FX_STANDALONE_ENABLE */ + + +#ifdef FX_REGRESSION_TEST + +/* Define parameters for regression test suite. */ + +#define FX_MAX_SECTOR_CACHE 256 +#define FX_MAX_FAT_CACHE 64 +#define FX_FAT_MAP_SIZE 1 + + +/* Define variables and macros used to introduce errors for the regression test suite. */ + +extern ULONG _fx_ram_driver_io_error_request; +extern ULONG _fx_ram_driver_io_request_count; +extern ULONG _fx_file_open_max_file_size_request; +extern ULONG _fx_directory_entry_read_count; +extern ULONG _fx_directory_entry_read_error_request; +extern ULONG _fx_directory_entry_write_count; +extern ULONG _fx_directory_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_write_count; +extern ULONG _fx_utility_fat_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_read_count; +extern ULONG _fx_utility_fat_entry_read_error_request; +extern ULONG _fx_utility_logical_sector_flush_count; +extern ULONG _fx_utility_logical_sector_flush_error_request; +extern ULONG _fx_utility_logical_sector_write_count; +extern ULONG _fx_utility_logical_sector_write_error_request; +extern ULONG _fx_utility_logical_sector_read_count; +extern ULONG _fx_utility_logical_sector_read_error_request; +extern ULONG _fx_utility_logical_sector_read_1_count; +extern ULONG _fx_utility_logical_sector_read_1_error_request; + +#ifdef FX_ENABLE_FAULT_TOLERANT +struct FX_MEDIA_STRUCT; +extern VOID fault_tolerant_enable_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +#endif /* FX_ENABLE_FAULT_TOLERANT */ + + +#define FX_DIRECTORY_ENTRY_READ_EXTENSION _fx_directory_entry_read_count++; \ + if (_fx_directory_entry_read_error_request) \ + { \ + _fx_directory_entry_read_error_request--; \ + if (_fx_directory_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_DIRECTORY_ENTRY_WRITE_EXTENSION _fx_directory_entry_write_count++; \ + if (_fx_directory_entry_write_error_request) \ + { \ + _fx_directory_entry_write_error_request--; \ + if (_fx_directory_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_READ_EXTENSION _fx_utility_fat_entry_read_count++; \ + if (_fx_utility_fat_entry_read_error_request) \ + { \ + _fx_utility_fat_entry_read_error_request--; \ + if (_fx_utility_fat_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 10000) \ + { \ + *entry_ptr = 1; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 20000) \ + { \ + *entry_ptr = media_ptr -> fx_media_fat_reserved; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 30000) \ + { \ + *entry_ptr = cluster; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 40000) \ + { \ + media_ptr -> fx_media_total_clusters = 0; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_WRITE_EXTENSION _fx_utility_fat_entry_write_count++; \ + if (_fx_utility_fat_entry_write_error_request) \ + { \ + _fx_utility_fat_entry_write_error_request--; \ + if (_fx_utility_fat_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_FLUSH_EXTENSION _fx_utility_logical_sector_flush_count++; \ + if (_fx_utility_logical_sector_flush_error_request) \ + { \ + _fx_utility_logical_sector_flush_error_request--; \ + if (_fx_utility_logical_sector_flush_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION _fx_utility_logical_sector_read_count++; \ + if (_fx_utility_logical_sector_read_error_request) \ + { \ + _fx_utility_logical_sector_read_error_request--; \ + if (_fx_utility_logical_sector_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION_1 _fx_utility_logical_sector_read_1_count++; \ + if (_fx_utility_logical_sector_read_1_error_request) \ + { \ + _fx_utility_logical_sector_read_1_error_request--; \ + if (_fx_utility_logical_sector_read_1_error_request == 0) \ + { \ + cache_entry = FX_NULL; \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_WRITE_EXTENSION _fx_utility_logical_sector_write_count++; \ + if (_fx_utility_logical_sector_write_error_request) \ + { \ + _fx_utility_logical_sector_write_error_request--; \ + if (_fx_utility_logical_sector_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_FAULT_TOLERANT_ENABLE_EXTENSION fault_tolerant_enable_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, total_size); +#define FX_FAULT_TOLERANT_APPLY_LOGS_EXTENSION fault_tolerant_apply_log_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, size); + +#endif /* FX_REGRESSION_TEST */ + /* Define FileX internal protection macros. If FX_SINGLE_THREAD is defined, these protection macros are effectively disabled. However, for multi-thread uses, the macros are setup to utilize a ThreadX mutex for multiple thread access control into an open media. */ +/* Reduce the mutex error checking for testing purpose. */ + #if defined(FX_SINGLE_THREAD) || defined(FX_STANDALONE_ENABLE) #define FX_PROTECT #define FX_UNPROTECT #else -#define FX_PROTECT if (media_ptr -> fx_media_id != FX_MEDIA_ID) return(FX_MEDIA_NOT_OPEN); \ - else if (tx_mutex_get(&(media_ptr -> fx_media_protect), TX_WAIT_FOREVER) != TX_SUCCESS) return(FX_MEDIA_NOT_OPEN); +#define FX_PROTECT tx_mutex_get(&(media_ptr -> fx_media_protect), TX_WAIT_FOREVER); #define FX_UNPROTECT tx_mutex_put(&(media_ptr -> fx_media_protect)); #endif @@ -131,6 +293,9 @@ typedef unsigned long long ULONG64; #define FX_DISABLE_INTS old_interrupt_posture = tx_interrupt_control(TX_INT_DISABLE); #define FX_RESTORE_INTS tx_interrupt_control(old_interrupt_posture); #else +#ifndef FX_LEGACY_INTERRUPT_PROTECTION +#define FX_LEGACY_INTERRUPT_PROTECTION +#endif #define FX_INT_SAVE_AREA #define FX_DISABLE_INTS #define FX_RESTORE_INTS @@ -166,15 +331,15 @@ typedef unsigned long long ULONG64; #endif /* Define the update rate of the system timer. These values may also be defined at the command - line when compiling the fx_system_initialize.c module in the FileX library build. Alternatively, they can - be modified in this file or fx_user.h. Note: the update rate must be an even number of seconds greater - than or equal to 2, which is the minimal update rate for FAT time. */ + line when compiling the fx_system_initialize.c module in the FileX library build. Alternatively, + they can be modified in this file or fx_user.h. Note: the update rate must be an even number of + seconds greater than or equal to 2, which is the minimal update rate for FAT time. */ /* Define the number of seconds the timer parameters are updated in FileX. The default value is 10 seconds. This value can be overwritten externally. */ #ifndef FX_UPDATE_RATE_IN_SECONDS -#define FX_UPDATE_RATE_IN_SECONDS 10 +#define FX_UPDATE_RATE_IN_SECONDS 10 #endif @@ -196,11 +361,9 @@ typedef unsigned long long ULONG64; #ifdef FX_SYSTEM_INIT CHAR _fx_version_id[] = - "Copyright (c) 2024 Microsoft Corporation. * FileX Win32/Version 6.5.1.202602 *"; - * Copyright (c) 2026-present Eclipse ThreadX contributors + "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win32/Visual 6.5.1.202602 *"; #else extern CHAR _fx_version_id[]; #endif -#endif - +#endif /* FX_PORT_H */ diff --git a/scripts/build_fx.ps1 b/scripts/build_fx.ps1 index 5407157..40f241e 100644 --- a/scripts/build_fx.ps1 +++ b/scripts/build_fx.ps1 @@ -1,3 +1,5 @@ +# Portions of this file were generated with AI assistance. + [CmdletBinding()] param( [ValidateSet('win64', 'win32')] @@ -28,7 +30,11 @@ if (-not $BuildDir) { } if (-not $ThreadXDir) { - $ThreadXDir = Join-Path (Split-Path -Parent $repoRoot) 'threadx-fd-codex' + $ThreadXDir = Join-Path (Split-Path -Parent $repoRoot) 'threadx' +} + +if (-not (Test-Path (Join-Path $ThreadXDir 'common\inc\tx_api.h'))) { + throw "No ThreadX source tree at $ThreadXDir. Clone eclipse-threadx/threadx beside this repository, or pass -ThreadXDir ." } $selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt index d6ccd66..b2168af 100644 --- a/test/cmake/CMakeLists.txt +++ b/test/cmake/CMakeLists.txt @@ -1,3 +1,5 @@ +# Portions of this file were generated with AI assistance. + cmake_minimum_required(VERSION 3.13 FATAL_ERROR) cmake_policy(SET CMP0054 NEW) cmake_policy(SET CMP0057 NEW) @@ -51,7 +53,13 @@ set(standalone_no_cache_fault_tolerant_build ${no_cache_build} ${FX_FAULT_TOLERA if(MSVC) add_compile_options(/W3 /Zi) - add_link_options(/DEBUG /INCREMENTAL:NO) + # /LARGEADDRESSAWARE lets the 32-bit (win32) test executables use the full + # 4 GB user address space available to a 32-bit process on 64-bit Windows, + # instead of the default 2 GB. The large-media regression tests allocate + # ~1.7 GB of RAM-disk + data buffers (e.g. filex_file_allocate_test), which + # overflows the 2 GB limit and makes calloc return NULL -> crash. This flag + # is the default for x64, so it is a no-op for the win64 build. + add_link_options(/DEBUG /INCREMENTAL:NO /LARGEADDRESSAWARE) # Both cl.exe and gcc accept -DFOO; use it to keep the per-configuration # definitions consistent with the Linux path. add_compile_options(-DFX_REGRESSION_TEST ${${CMAKE_BUILD_TYPE}}) diff --git a/test/cmake/regression/CMakeLists.txt b/test/cmake/regression/CMakeLists.txt index c806e5d..14145f3 100644 --- a/test/cmake/regression/CMakeLists.txt +++ b/test/cmake/regression/CMakeLists.txt @@ -154,9 +154,21 @@ endif() foreach(test_case ${regression_test_cases}) get_filename_component(test_name ${test_case} NAME_WE) - add_executable(${test_name} ${test_case}) - target_link_libraries(${test_name} PRIVATE test_utility) - add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name}) + if(MSVC) + # CMake repeats the target name in its object directory. Several descriptive + # FileX test names otherwise exceed the legacy path limit used by cl.exe in a + # normally nested Windows worktree. + string(MD5 test_target_hash ${test_name}) + string(SUBSTRING ${test_target_hash} 0 12 test_target_hash) + set(test_target fx_test_${test_target_hash}) + else() + set(test_target ${test_name}) + endif() + + add_executable(${test_target} ${test_case}) + target_link_libraries(${test_target} PRIVATE test_utility) + set_target_properties(${test_target} PROPERTIES OUTPUT_NAME ${test_name}) + add_test(NAME ${CMAKE_BUILD_TYPE}::${test_name} COMMAND ${test_target}) endforeach() # These two tests write or corrupt large amounts of data on a 900 MB RAM disk and are diff --git a/test/regression_test/filex_directory_create_delete_test.c b/test/regression_test/filex_directory_create_delete_test.c index d092469..9ad4ca7 100644 --- a/test/regression_test/filex_directory_create_delete_test.c +++ b/test/regression_test/filex_directory_create_delete_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This FileX test concentrates on the basic directory create/delete operations. */ #ifndef FX_STANDALONE_ENABLE @@ -111,8 +113,10 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; +#ifdef FX_ENABLE_FAULT_TOLERANT static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; #endif +#endif extern ULONG _fx_ram_driver_copy_default_format; @@ -3064,4 +3068,3 @@ FX_DIR_ENTRY search_directory; test_control_return(0); } } - diff --git a/test/regression_test/filex_directory_duplicate_entries_test.c b/test/regression_test/filex_directory_duplicate_entries_test.c index 5266d66..1ff2379 100644 --- a/test/regression_test/filex_directory_duplicate_entries_test.c +++ b/test/regression_test/filex_directory_duplicate_entries_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This FileX test concentrates on recovery operation when there are duplicate entries. */ #ifndef FX_STANDALONE_ENABLE @@ -49,8 +51,10 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; +#ifdef FX_ENABLE_FAULT_TOLERANT static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; #endif +#endif extern ULONG _fx_ram_driver_copy_default_format; diff --git a/test/regression_test/filex_directory_rename_test.c b/test/regression_test/filex_directory_rename_test.c index 36fd181..f76e8fb 100644 --- a/test/regression_test/filex_directory_rename_test.c +++ b/test/regression_test/filex_directory_rename_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This FileX test concentrates on the directory rename operations. */ #ifndef FX_STANDALONE_ENABLE @@ -47,8 +49,10 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; +#ifdef FX_ENABLE_FAULT_TOLERANT static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; #endif +#endif /* Define thread prototypes. */ diff --git a/test/regression_test/filex_file_create_delete_test.c b/test/regression_test/filex_file_create_delete_test.c index d296ba4..43d01bb 100644 --- a/test/regression_test/filex_file_create_delete_test.c +++ b/test/regression_test/filex_file_create_delete_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This FileX test concentrates on the file create/delete operations. */ #ifndef FX_STANDALONE_ENABLE @@ -54,8 +56,10 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; +#ifdef FX_ENABLE_FAULT_TOLERANT static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; #endif +#endif static UCHAR fat_buffer[128]; static UCHAR name_buffer[FX_MAX_LONG_NAME_LEN+1]; diff --git a/test/regression_test/filex_file_rename_test.c b/test/regression_test/filex_file_rename_test.c index c6c104e..3ff5bbd 100644 --- a/test/regression_test/filex_file_rename_test.c +++ b/test/regression_test/filex_file_rename_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + /* This FileX test concentrates on the file rename operations. */ #ifndef FX_STANDALONE_ENABLE @@ -46,8 +48,10 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; +#ifdef FX_ENABLE_FAULT_TOLERANT static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; #endif +#endif /* Define thread prototypes. */ diff --git a/test/regression_test/fx_ram_driver_test.h b/test/regression_test/fx_ram_driver_test.h index df9e610..be264e3 100644 --- a/test/regression_test/fx_ram_driver_test.h +++ b/test/regression_test/fx_ram_driver_test.h @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +// Portions of this file were generated with AI assistance. + #ifndef _FX_RAM_DRIVER_TEST_H_ #define _FX_RAM_DRIVER_TEST_H_ @@ -24,15 +26,28 @@ /* Define the memory buffer to format the media if enable fault tolerant feature.*/ +/* On MSVC these are heap-allocated (see fx_ram_driver_test.c) to avoid committing + huge BSS arrays and exceeding the 2 GB PE image limit, so declare them as + pointers there; other compilers keep them as BSS arrays. */ +#ifdef _MSC_VER +extern UCHAR *ram_disk_memory_large; +extern UCHAR *large_data_buffer; +#else extern UCHAR ram_disk_memory_large[900000000]; extern UCHAR large_data_buffer[900000000]; +#endif #define large_data_buffer_size 900000000 /* Define memory for tests to be run in standalone mode (without Azure RTOS: ThreadX) */ #ifdef FX_STANDALONE_ENABLE +#ifdef _MSC_VER +extern UCHAR *ram_disk_memory; +extern UCHAR *ram_disk_memory1; +#else extern UCHAR ram_disk_memory[300000000]; extern UCHAR ram_disk_memory1[30000000]; #endif +#endif /* Define a macro for test. */ /* We do not need error code anymore but still define return_value_if_fail for compatibility backward. */ From fd07daa08dbdce9e22dd131c1f8291f88bc2be0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:06:02 -0400 Subject: [PATCH 07/14] Added a blame ignore list for the mechanical header and version passes (#102) Running git blame on a file header returns a tree-wide copyright, disclosure or version-constant pass rather than the commit that last touched the code. There have been 3 such passes in this repository since 2024, and every header line in the tree now points at one of them. Added .git-blame-ignore-revs listing those commits. GitHub applies the file to its blame view automatically; locally it takes one git config command, which the file documents in its own header. Additive. No source file is touched, and every listed commit was verified to be an ancestor of dev. Assisted-by: Claude Code (Opus 5) --- .git-blame-ignore-revs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..b2a76c2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,22 @@ +# Commits that changed file headers, copyright lines or version constants across +# the whole tree. They carry no behavioural change, so `git blame` should look +# through them to the commit that last touched the code itself. +# +# GitHub applies this file automatically. To use it locally, run once: +# +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# Add a commit here only when it is mechanical and repository-wide. A commit +# that changes behaviour does not belong in this file, however large it is. + +# 2024-01-29 Update copyright. +# 229 files +8c3b71832c6650e022cac3c824b8cda4b728248e + +# 2026-03-05 Updated copyright headers and removed revision history +# 372 files +61b0b06c3eca2690c2b80cadb09321d1479bd013 + +# 2026-06-06 Added copyright headers to files missing them +# 143 files +30e1d491a92c03cf8f52d1ef28fd017eb9bfb268 From 2ae29009e36b421075481b2554834956e3a7dbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:06:32 -0400 Subject: [PATCH 08/14] Linked FileX readers to published documentation (#105) The FileX README sent readers to archived Markdown documentation. I linked the overview to its published HTML guide and the general documentation entry to the latest release. Both replacement URLs returned HTTP 200; git diff --check passed. Assisted-by: Codex (gpt-6-astra) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 745371f..2200980 100755 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ FileX as part of Eclipse ThreadX has been integrated to the semiconductor's SDKs We also provide [samples](https://github.com/eclipse-threadx/samples) using hero development boards from semiconductors you can build and test with. -See [Overview of Eclipse ThreadX FileX](https://github.com/eclipse-threadx/rtos-docs/blob/main/rtos-docs/filex/overview-filex.md) for the high-level overview. +See [Overview of Eclipse ThreadX FileX](https://threadx.io/releases/6.5.1/filex/main/overview-filex.html) for the high-level overview. ## Repository Structure and Usage @@ -129,7 +129,7 @@ License terms for using Eclipse ThreadX are defined in the LICENSE.txt file of t The following are references to additional Eclipse ThreadX resources: -- **Product introduction**: https://github.com/eclipse-threadx/rtos-docs +- **Product introduction**: https://threadx.io/releases/latest - **Product issues and bugs, or feature requests**: https://github.com/eclipse-threadx/filex/issues - **TraceX Installer**: https://aka.ms/azrtos-tracex-installer From 3f8b4f7e923dffc33141ed4536f0df9e2e7deb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:06:51 -0400 Subject: [PATCH 09/14] Updated FileX contribution guide for current workflows (#107) * Updated FileX contribution guide for current workflows The existing guide omitted FileX build, test, and submission practices. The guide now covers the project process and FileX C99, Linux and Windows regression configurations, coverage, attribution, and release practices. It also states that the regression workflow does not run for pull requests to dev. Local Markdown links resolve and git diff --check passed. Build tests were not run for this documentation change. Assisted-by: Codex (gpt-6-sol) * Documented FileX dev regression and coverage gates The contributor guide described the old master-only workflow and coverage from selected Linux configurations. The guide now covers all nine Linux coverage configurations, the Win64 CI job, Win32 local testing, pinned dependencies, and the merged coverage floors. git diff --check passed. Build tests were not run for this documentation change. Assisted-by: Codex (gpt-6-sol) * Added visible spacing between contributor setup steps The ECA and Git setup items had no visible gap in GitHub Markdown. An indented line break now separates them while preserving list numbering. GitHub Markdown rendered the items in one list with the visible gap, and git diff --check passed. Assisted-by: Codex (gpt-6-sol) --- CONTRIBUTING.md | 375 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 331 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a327a0b..f7d2eae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,72 +1,359 @@ -# Contributing to Eclipse ThreadX +# Contributing to Eclipse ThreadX FileX Thanks for your interest in this project. ## Project description -Eclipse ThreadX provides a vendor-neutral, open source, safety certified OS for -real-time applications published on under a permissive license. The Eclipse -ThreadX suite encompasses: -* ThreadX - advanced real-time operating system (RTOS) designed specifically for deeply embedded applications -* NetX Duo - advanced, industrial-grade TCP/IP network stack designed specifically for deeply embedded real-time and IoT applications -* FileX - high-performance, FAT-compatible file system that’s fully integrated with ThreadX kernel -* GUIX - provides a complete, embedded graphical user interface (GUI) library -* USBX - high-performance USB host, device, and on-the-go (OTG) embedded stack, that is fully integrated with ThreadX kernel -* LevelX - Flash Wear Leveling for FileX and stand-alone purposes -* GuiX Studio - design environment, facilitating the creation and maintenance of all graphical elements for GUIX -* TraceX - analysis tool that provides a graphical view of real-time system events to better understand the behavior of real-time systems +FileX is the Eclipse ThreadX FAT-compatible file system for deeply embedded applications. It provides file management over media such as RAM, SD cards, USB storage, and NAND or NOR flash through LevelX. The repository contains the core file system, media drivers, samples, and processor ports. FileX integrates with ThreadX and also supports a standalone build. -Project site: https://projects.eclipse.org/projects/iot.threadx +Eclipse ThreadX provides a vendor-neutral, open source, safety-certified OS for real-time applications, published under a permissive license. The Eclipse ThreadX suite encompasses: + +* **ThreadX** - advanced real-time operating system (RTOS) designed specifically for deeply embedded applications +* **NetX Duo** - advanced, industrial-grade TCP/IP network stack designed specifically for deeply embedded real-time and IoT applications +* **FileX** - high-performance, FAT-compatible file system that is fully integrated with the ThreadX kernel +* **GUIX** - complete, embedded graphical user interface (GUI) library +* **GUIX Studio** - design environment, facilitating the creation and maintenance of all graphical elements for GUIX +* **USBX** - high-performance USB host, device, and on-the-go (OTG) embedded stack, fully integrated with the ThreadX kernel +* **LevelX** - flash wear levelling for FileX and stand-alone purposes +* **TraceX** - analysis tool that provides a graphical view of real-time system events to better understand the behaviour of real-time systems +* **ZoneX** - deterministic partitioning hypervisor for mixed-criticality embedded systems +* **SampleX** - samples and demos for the components above + +Project websites: + +* https://threadx.io +* https://projects.eclipse.org/projects/iot.threadx + +This file describes how to contribute to **FileX**. General contribution conventions are shared across the Eclipse ThreadX repositories; build and test instructions differ by component. ## Terms of Use -This repository is subject to the Terms of Use of the Eclipse Foundation -https://www.eclipse.org/legal/termsofuse.php +This repository is subject to the Terms of Use of the Eclipse Foundation: https://www.eclipse.org/legal/termsofuse.php -## Developer resources +## New contributors -Information regarding source code management, builds, coding standards, and more. -https://projects.eclipse.org/projects/iot.threadx/developer +Welcome. Here is the shortest path from zero to a merged pull request. -The project maintains the following source code repositories +1. **Create an Eclipse Foundation account.** https://accounts.eclipse.org/user/register -* https://github.com/eclipse-threadx/.github -* https://github.com/eclipse-threadx/cmsis-packs -* https://github.com/eclipse-threadx/filex -* https://github.com/eclipse-threadx/getting-started -* https://github.com/eclipse-threadx/guix -* https://github.com/eclipse-threadx/levelx -* https://github.com/eclipse-threadx/netxduo -* https://github.com/eclipse-threadx/rtos-docs -* https://github.com/eclipse-threadx/samples -* https://github.com/eclipse-threadx/threadx -* https://github.com/eclipse-threadx/threadx-learn-samples -* https://github.com/eclipse-threadx/tracex -* https://github.com/eclipse-threadx/usbx + The email address on that account matters: it must be the same address you use as the `Author` of your Git commits. If the two do not match, the ECA check on your pull request will fail. + +2. **Sign the Eclipse Contributor Agreement (ECA).** See the section below. This is a one-time step and covers every Eclipse Foundation project. + +
+ +3. **Configure Git accordingly.** + + ``` + git config user.name "Your Name" + git config user.email "the-address-on-your-eclipse-account@example.org" + ``` + +4. **Pick something to work on.** Open issues are fair game, including ones nobody has assigned. Please leave a comment on the issue saying you intend to work on it, so two people do not solve the same problem twice. If an issue carries a `good first issue` label, it is a reasonable starting point. + +5. **Have an idea for a new feature? Discuss it first.** Open a thread in [GitHub Discussions](https://github.com/orgs/eclipse-threadx/discussions) or raise it on the [developer mailing list](https://accounts.eclipse.org/mailing-list/threadx-dev) before writing code. File system and media behaviour can affect stored data across many applications, so discuss the shape of a feature before submitting a finished implementation. + +Bug fixes and documentation corrections need no prior discussion. Send them straight in. ## Eclipse Development Process -This Eclipse Foundation open project is governed by the Eclipse Foundation -Development Process and operates under the terms of the Eclipse IP Policy. +This Eclipse Foundation open project is governed by the Eclipse Foundation Development Process and operates under the terms of the Eclipse IP Policy. * https://eclipse.org/projects/dev_process * https://www.eclipse.org/org/documents/Eclipse_IP_Policy.pdf ## Eclipse Contributor Agreement -In order to be able to contribute to Eclipse Foundation projects you must electronically sign the Eclipse Contributor Agreement (ECA). -https://www.eclipse.org/legal/ECA.php +In order to be able to contribute to Eclipse Foundation projects you must electronically sign the Eclipse Contributor Agreement (ECA): https://www.eclipse.org/legal/ECA.php + +The ECA provides the Eclipse Foundation with a permanent record that you agree that each of your contributions will comply with the commitments documented in the Developer Certificate of Origin (DCO). Having an ECA on file associated with the email address matching the "Author" field of your contribution's Git commits fulfills the DCO's requirement that you sign-off on your contributions. + +For more information, please see the Eclipse Committer Handbook: https://www.eclipse.org/projects/handbook/#resources-commit + +## Required tooling + +Build and test FileX with CMake and Ninja. The root and regression projects require CMake 3.13 or later. The regression CMake project sets C99 with compiler extensions off. GCC 14 is the project reference compiler on Linux. The Windows simulator uses MSVC from the Visual Studio Build Tools; it does not require the Visual Studio IDE. + +On Ubuntu, `scripts/install.sh` installs the host test tools and pins `gcovr` 8.6 in a Python virtual environment; activate that environment before collecting coverage locally. Use GCC 14 with matching gcov for the Linux regression suite. Embedded ports use the appropriate target compiler and toolchain file. Use GCC syntax for assembly built with GNU toolchains. + +A non-standalone FileX build links ThreadX. The Linux regression runner obtains the pinned ThreadX revision under `test/cmake/threadx/` when needed and checks an existing checkout against that pin. On Windows, pass the checkout explicitly to `scripts/build_fx.ps1` with `-ThreadXDir`. + +## Building and testing + +The regression suite lives under `test/cmake/` and defines nine configurations covering the normal build, disabled cache, disabled error checking, fault tolerance, and standalone combinations. The Linux wrapper scripts build and test all configurations: + +```sh +CC=gcc-14 GCOV=gcov-14 TX_COVERAGE=ON scripts/build.sh +CC=gcc-14 GCOV=gcov-14 TX_COVERAGE=ON scripts/test.sh +``` + +For a focused Linux run, call the CMake suite runner with the same configuration for the build and test steps: + +```sh +test/cmake/run.sh build fault_tolerant_build_coverage +test/cmake/run.sh test fault_tolerant_build_coverage +``` + +The Windows simulator has `win32` and `win64` ports. Its PowerShell scripts configure CMake with Ninja and use MSVC from the Build Tools. For a non-standalone run, supply a ThreadX checkout: + +```powershell +scripts/build_fx.ps1 -Arch win64 -ThreadXDir 'path/to/threadx' +scripts/test_fx.ps1 -Arch win64 +``` + +Use `-Arch win32` for the 32-bit simulator. Both PowerShell scripts accept `-Configuration` to select configurations; the Windows tests run serially because the simulator is timing-sensitive. The Linux test wrapper uses the repository's CTest runner. Report the architecture, configurations, and results that exercised your change. + +With `TX_COVERAGE=ON`, all nine Linux configurations instrument the FileX library. `test/cmake/coverage.sh` writes per-configuration JSON, XML and HTML reports and merges them across the full suite. The merged report excludes the sample RAM driver and must meet 99.9% line and 99.4% branch coverage in CI. Coverage is not collected by the MSVC Windows build. The project goal is 100% test coverage: add or update regression tests for new behaviour and explain any relevant gaps. File-system changes involving a media driver also need testing on the affected media or hardware when available. + +## Continuous integration + +`regression_test.yml` runs on pushes and pull requests to `dev` and `master`, and by manual dispatch. Its Linux job builds and tests all nine configurations and merges their coverage through the pinned ThreadX reusable regression workflow. A separate Win64 job builds and tests all nine configurations with the PowerShell scripts. The workflow does not run Win32 or hardware tests. + +Run relevant Win32 or hardware tests locally and report their results in the pull request. Do not describe an unrun configuration or hardware target as verified. + +## Pull request acceptance criteria + +**Pull requests must target the `dev` branch.** `master` holds the latest release; see [Release model and support](#release-model-and-support) below. A pull request opened against `master` will be asked to retarget. + +Before requesting a review, check your contribution against this list. + +**Process** + +* The branch is a feature branch based on `dev`. Never commit directly to `master` or `dev`. +* Your ECA is signed and the commit `Author` email matches your Eclipse account. +* The pull request is one logical change. Unrelated fixes belong in separate pull requests. +* Commit subject lines start with a past-tense verb, for example `Fixed the memory allocator`. +* The pull request explains what changed and why, and how you verified it. + +**Code** + +* The code is C99-compatible. +* It follows the coding style of the surrounding code. +* New functions and structures are documented in comments, as in existing code. +* MISRA C rules are followed as closely as practical, taking MISRA C 2004, 2012 and 2023 into account. Any deviation is explicit, names the rule being circumvented, and justifies it in a comment. +* `goto` is not used. +* Do not add new external dependencies. This is a hard rule. ThreadX is an existing component relationship for non-standalone builds. +* When implementing an industry standard, code is not copied from an existing implementation. Existing implementations may inform your work, but you must identify those sources clearly. +* Code is written with the suite's priorities in mind: runtime speed and small code size. +* New C and assembly files, and edited C and assembly files, carry the appropriate header and attribution (see below). + +**Verification** + +* All applicable CI checks are green, including the Linux and Win64 regression jobs on a pull request to `dev`. +* The change builds without new warnings on the reference toolchains. +* Regression tests covering the change are added or updated. The project targets 100% test coverage; a pull request that lowers coverage needs a stated reason. State which FileX configurations ran and any relevant coverage gap. +* API or behaviour changes come with a matching documentation pull request against [rtos-docs-asciidoc](https://github.com/eclipse-threadx/rtos-docs-asciidoc). + +**Security** + +If you discover a security issue while working on a change, do not describe it in a public pull request. Follow [SECURITY.md](SECURITY.md) instead. + +## AI-assisted contributions + +**AI-assisted contributions are welcome**, provided they are attributed. + +Two things are non-negotiable: + +1. **Attribution.** Mark AI-assisted C and assembly files as described below, and identify the tool and model in the commit message. +2. **Human responsibility.** The human contributor submitting the pull request is responsible for the contribution - technically and legally. Signing the ECA means *you* certify the contribution's provenance. An AI tool cannot sign the ECA and cannot hold that responsibility. Review what the tool produced, understand it, and verify that it is correct and that you have the right to contribute it. "The model wrote it" is not a defence. + +This is consistent with the Eclipse Foundation's [Generative AI Usage Guidelines](https://www.eclipse.org/projects/guidelines/genai/) and the [Eclipse Project Handbook](https://www.eclipse.org/projects/handbook/#genai). Please read them before submitting AI-assisted work. + +### Using a coding agent + +Give a coding agent this contribution guide and the build instructions for the area it changes. Review its output against the same acceptance criteria as any other contribution. In particular, check C99 compatibility, surrounding style, test coverage, and the relevant media and standalone or ThreadX configuration for runtime verification. + +### Header for new files + +Add this header when creating a new C or assembly (`.S`) file: + +```c +/*************************************************************************** + * Copyright (c) Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by (). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ +``` + +Substitute the current year, product name, and model and version. If a file was written without AI assistance, omit the AI Disclosure paragraph and use `SPDX-License-Identifier: MIT`. + +### Header for existing files + +When editing an existing C or assembly file whose copyright is from 2025 or earlier, and which does not already mention Eclipse ThreadX contributors, add this line to the header: + +```c + * Copyright (c) Eclipse ThreadX contributors +``` + +It goes *below* the older copyright. All copyright lines must stay in chronological order. For example: + +```c +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ +``` + +If the edit was AI-assisted and the file does not already carry an AI disclosure line, add this line just under the header: + +```c +// Portions of this file were generated with AI assistance. +``` -The ECA provides the Eclipse Foundation with a permanent record that you agree -that each of your contributions will comply with the commitments documented in -the Developer Certificate of Origin (DCO). Having an ECA on file associated with -the email address matching the "Author" field of your contribution's Git commits -fulfills the DCO's requirement that you sign-off on your contributions. +Use the comment marker already used by the file. Keep an existing disclosure line unchanged and never add a second one. If you copy an existing file to get started on a new one, treat the result as a new file: use the new-file header, do not carry the old one over. -For more information, please see the Eclipse Committer Handbook: -https://www.eclipse.org/projects/handbook/#resources-commit +### Commit attribution + +Attribute AI assistance with an `Assisted-by` trailer in each commit that contains AI-assisted changes: + +```text +Fixed the file write status + +A completed write could report an out-of-date status to the caller. + +The write path now returns the status set by the completed operation. + +The affected FileX regression configurations passed all tests. + +Assisted-by: () +``` + +Use the product's accepted name (`Claude Code`, `Copilot`, `Codex`, or `Gemini`) and the model string reported by the tool. Do not use `Co-Authored-By` for AI attribution. Commit subjects and pull request titles start with a past-tense verb and stay within 72 characters. The message body states the cause, fix, and test result in that order; wrap commit bodies at 88 characters. Pull request descriptions use one line per paragraph. + +## Release model and support + +This section summarises the project's [Release Model and Support Policy](https://github.com/eclipse-threadx/rtos-docs-asciidoc/blob/main/rtos-docs/home/modules/ROOT/pages/releases-and-support.adoc), which is the authoritative version. + +### Version numbers + +Eclipse ThreadX releases generally follow [Semantic Versioning](https://semver.org/). Given a version number **X.Y.Z.Bh**, for example `6.5.0.202601a`: + +* **X** increases for a *milestone release*. Components currently stay at version 6. +* **Y** increases for a *feature release* adding a major feature. +* **Z** increases for a *maintenance release* of minor fixes and improvements. +* **B** is a *build number* identifying the quarter of publication - `202601` is Q1 2026. +* **h** denotes a *hotfix release*, identified by a letter. Hotfix releases are component-specific. + +### Release cadence + +The project adopted a predictable quarterly release model in September 2025 and has published a release every quarter since. + +Quarterly releases ship new versions of every component, whether or not that component saw code changes, so that version numbers do not drift apart between components. A release shipping no code changes for a component says so in its release notes. + +Urgent fixes between quarterly releases ship as *hotfix* releases. Those contain only security fixes, or fixes for serious problems that prevent building or testing applications. + +### Branches + +The project runs a time-based release train on trunk-based development. There are no long-term support branches and no backports to older releases. + +* `master` always contains the latest quarterly or hotfix release. +* `dev` is the integration branch. Pull requests are merged there during the quarter. Track `dev` if you want to test new features and fixes ahead of a release. +* A quarterly release is performed by merging `dev` into `master`. + +### Support policy + +Issues in the codebase are fixed on a best-effort basis, and pull requests fixing existing or new issues are gladly accepted. + +Security vulnerabilities disclosed confidentially are handled under the [Eclipse Foundation's security policy](https://www.eclipse.org/security/policy/). Once a candidate vulnerability is confirmed, the team works to deliver a fix as soon as possible, shipping it in the next quarterly release or as a hotfix depending on timing. Resolved vulnerabilities are disclosed immediately after a release containing the fix becomes available. The team aims to resolve newly confirmed vulnerabilities within three months; that period may be extended by the Project Leadership Chain together with the Eclipse Foundation Security team where appropriate. + +See [SECURITY.md](SECURITY.md) for how to report a vulnerability. + +### Roadmap + +The project team plans its work on a [public GitHub project board](https://github.com/orgs/eclipse-threadx/projects/2/views/2). + +### A note on older releases + +Microsoft contributed only the Azure RTOS codebase v6.x to the Eclipse Foundation. Older ThreadX releases (v5.x and lower) sold by Express Logic were never made open source, and Microsoft has discontinued sales and support for them. Users of ThreadX 5.x and lower should upgrade to the latest release of Eclipse ThreadX as soon as possible. + +## Documentation + +The documentation is published at **https://threadx.io**. PDF manuals for every component, in A4 and US Letter formats, are attached as assets to each GitHub release. + +### The stack + +Documentation is written in [AsciiDoc](https://asciidoc.org/) and built with [Antora](https://antora.org/). PDF manuals are produced by the [Antora Assembler](https://docs.antora.org/assembler/latest/) with `asciidoctor-pdf`. + +### The workflow + +1. The single source of truth is [rtos-docs-asciidoc](https://github.com/eclipse-threadx/rtos-docs-asciidoc). All documentation changes are made there, as pull requests, following the same branch rules as the code repositories. +2. At release time the site is generated from that source into [rtos-docs-html](https://github.com/eclipse-threadx/rtos-docs-html), which holds the rendered HTML and exists solely for website integration. Do not edit it by hand - your changes will be overwritten by the next build. +3. The website then serves the generated content from `rtos-docs-html`. + +The older `rtos-docs` repository, which held the documentation in Markdown, is archived and superseded by `rtos-docs-asciidoc`. Do not send changes there. + +If your contribution adds or changes an API or a feature, open a matching pull request against `rtos-docs-asciidoc`. + +## Developer resources + +Information regarding source code management, builds, coding standards, and more: https://projects.eclipse.org/projects/iot.threadx/developer + +The project maintains the following repositories: + +**Components** + +* https://github.com/eclipse-threadx/threadx +* https://github.com/eclipse-threadx/netxduo +* https://github.com/eclipse-threadx/filex +* https://github.com/eclipse-threadx/guix +* https://github.com/eclipse-threadx/usbx +* https://github.com/eclipse-threadx/levelx +* https://github.com/eclipse-threadx/tracex +* https://github.com/eclipse-threadx/zonex + +**Samples and platforms** + +* https://github.com/eclipse-threadx/samplex +* https://github.com/eclipse-threadx/supported-platforms + +**Documentation** + +* https://github.com/eclipse-threadx/rtos-docs-asciidoc +* https://github.com/eclipse-threadx/rtos-docs-html + +**Community and process** + +* https://github.com/eclipse-threadx/discussions +* https://github.com/eclipse-threadx/trustedx +* https://github.com/eclipse-threadx/.github ## Contact -Contact the project developers via the project's "dev" list. +### GitHub Discussions + +https://github.com/orgs/eclipse-threadx/discussions + +Q&A, feedback, and announcements. Decisions taken by the project team are documented here as well. This is usually the fastest way to reach both the team and other users. + +### Main ThreadX mailing list + +https://accounts.eclipse.org/mailing-list/threadx + +News and updates about the ThreadX project and the ThreadX Alliance. + +### Developer mailing list + https://accounts.eclipse.org/mailing-list/threadx-dev + +Project team conversations. Feel free to jump in and ask non-technical questions there. + +### User mailing list + +https://accounts.eclipse.org/mailing-list/threadx-users + +Ask your technical questions and discuss issues here. From ba68796eeed62dfeec76df93ba1fa1c68b04e854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:07:11 -0400 Subject: [PATCH 10/14] Added a check that keeps the AI disclosure comment in its accepted form (#103) * Added a check that keeps the AI disclosure comment in its one accepted form Nothing enforced the disclosure convention. In the sibling repositories the drift it exists to prevent returned twice, once when a port merged after the normalisation pass carrying the older per-edit form, and once because that pass had covered source files only. This repository is currently clean, and this keeps it so. Added scripts/check_ai_disclosure.sh, which rejects the superseded per-edit form, a doubled comment marker, more than one disclosure line in a file, and any spelling of the line that is not exact. It runs from repo_checks.yml, a workflow with no path filter, because a source-path filter is what hid the build files elsewhere. The check passes on this repository as it stands. Each of its four rules was confirmed to fail on a tree with that defect reintroduced, and to pass once it was removed. Assisted-by: Claude Code (Opus 5) * Exempted Markdown from the AI disclosure near-miss check The near-miss check matches the bare phrase "AI assistance" on any line that is not the accepted text. A contribution guide has to quote the accepted line and say when it applies, so #107 turns this check red although each of the two changes is green on its own. Markdown is now excluded from the near-miss check alone. A superseded form in a Markdown file is still caught by the first check, which looks for the product and model wording the fixed line replaced. Verified on dev with #107 merged: the check passes. It still fails on a planted near miss in a C source, on a planted superseded form in a Markdown file, and on a planted duplicate disclosure line. Assisted-by: Claude Code (Opus 5) --- .github/workflows/repo_checks.yml | 63 +++++++++++++++ scripts/check_ai_disclosure.sh | 129 ++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 .github/workflows/repo_checks.yml create mode 100755 scripts/check_ai_disclosure.sh diff --git a/.github/workflows/repo_checks.yml b/.github/workflows/repo_checks.yml new file mode 100644 index 0000000..955e6dd --- /dev/null +++ b/.github/workflows/repo_checks.yml @@ -0,0 +1,63 @@ +############################################################################### +# Copyright (c) 2026 Eclipse ThreadX contributors +# +# This program and the accompanying materials are made available under the +# terms of the MIT License which is available at +# https://opensource.org/licenses/MIT. +# +# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). +# The AI-generated portions may be considered public domain (CC0-1.0) +# and not subject to the project's licence. The human contributor has +# reviewed and verified that the code is correct. +# +# SPDX-License-Identifier: MIT and CC0-1.0 +############################################################################### + +name: repo_checks + +# Repository-wide text checks. +# +# THIS LIVES IN ITS OWN WORKFLOW BECAUSE IT MUST NOT BE PATH-FILTERED, and a +# path filter is a property of a workflow rather than of a job. The check +# selects its input with `git ls-files` and scans every tracked file, so any +# file at all can carry a finding. +# +# That is not hypothetical here. The pass that introduced the fixed +# disclosure line covered source files only, and the drift it was meant to end +# survived for months afterwards in CMake files, toolchain files, shell and +# PowerShell scripts, a GDB script and a Visual Studio manifest -- precisely +# the files a source-path filter would have skipped. A workflow that gates no +# pull request anybody opens is worse than no workflow, because it looks like +# coverage. +# +# Cheap enough that running it on everything costs nothing worth measuring: it +# is grep over a repository this size, with no toolchain, no build and no +# cache. + +on: + # No `paths:` on either trigger, deliberately. See above. + push: + branches: [ dev, master ] + pull_request: + branches: [ dev, master ] + +# A second push to the same branch makes the first answer irrelevant. +concurrency: + group: repo-checks-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-24.04 + steps: + # Actions are pinned to a commit SHA with the version in the trailing + # comment. A tag can be moved; a SHA cannot, which is what makes "which + # code ran in our CI" answerable from the repository. + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check the AI disclosure comments + run: scripts/check_ai_disclosure.sh diff --git a/scripts/check_ai_disclosure.sh b/scripts/check_ai_disclosure.sh new file mode 100755 index 0000000..0a76976 --- /dev/null +++ b/scripts/check_ai_disclosure.sh @@ -0,0 +1,129 @@ +#!/bin/bash +############################################################################### +# Copyright (c) 2026 Eclipse ThreadX contributors +# +# This program and the accompanying materials are made available under the +# terms of the MIT License which is available at +# https://opensource.org/licenses/MIT. +# +# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). +# The AI-generated portions may be considered public domain (CC0-1.0) +# and not subject to the project's licence. The human contributor has +# reviewed and verified that the code is correct. +# +# SPDX-License-Identifier: MIT and CC0-1.0 +############################################################################### +# +# Fail the build if a file's AI disclosure comment has drifted from the one +# accepted form. +# +# A file that was edited with AI assistance carries exactly one line: +# +# Portions of this file were generated with AI assistance. +# +# written with the comment character that file already uses. It names no +# product, no model and no version, and a file carries at most one of it, ever. +# +# The text is fixed for a reason that is easy to miss. An earlier convention +# named the product and the model -- "Some portions generated by +# ()" -- and the result was not attribution but accumulation: each tool +# that touched a file failed to recognise the line another tool had left, and +# appended its own. Files reached three stacked lines, and one product ended +# up spelled four different ways across the tree, which made the record +# unusable for the one question it was meant to answer. +# +# Precise attribution lives on the commit instead, where the Assisted-by +# trailer is dated and attached to the diff it describes: +# +# git log --format='%h %(trailers:key=Assisted-by,valueonly)' -- +# +# A file-level flag answers WHETHER; the history answers WHO. A header line +# cannot hold the second honestly, because the code it names gets rewritten +# and the line stays. +# +# The AI Disclosure paragraph in a new file's copyright header is different and +# is not checked here: it keeps its product and model version, because a file +# is created once and that record cannot grow. +# +# This script is excluded from its own scan. It has to spell the rejected +# forms in order to look for them. + +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)" +readonly SELF='scripts/check_ai_disclosure.sh' +readonly FIXED='Portions of this file were generated with AI assistance.' + +cd "${ROOT}" + +# Tracked files only. A build tree is not this repository's text to police, +# and scanning one would make the check depend on whether somebody had built. +mapfile -d '' -t FILES < <(git ls-files -z | grep -zZv "^${SELF}$") + +status=0 + +report() { + printf '%s\n\n' "$1" >&2 + printf '%s\n\n' "$2" >&2 + status=1 +} + +# 1. The superseded per-edit form, which names a product and a model. +hits="$(grep -nI 'Some portions generated by' -- "${FILES[@]}" 2>/dev/null || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: superseded per-edit form. + +Replace each of these with the fixed line, keeping the file's comment +character: + + ${FIXED}" "${hits}" +fi + +# 2. A doubled comment marker, such as '; //' or '@ //'. Assembly dialects +# differ -- armasm and IAR use ';', GNU as uses '@' or '//' -- and writing +# both is a symptom of a tool guessing rather than reading the file. +hits="$(grep -nIE '(//|[;@#])[[:space:]]*//[[:space:]]*Portions of this file were generated' \ + -- "${FILES[@]}" 2>/dev/null || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: doubled comment marker. + +Use the single comment character the rest of the file uses." "${hits}" +fi + +# 3. More than one disclosure line in a file. This is the failure the fixed +# text exists to prevent, so it is worth catching directly rather than +# inferring it from the form. +hits="$(grep -cIF "${FIXED}" -- "${FILES[@]}" 2>/dev/null | awk -F: '$NF > 1' || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: more than one disclosure line. + +A file carries at most one, ever. Keep the first and delete the rest; the +commit trailer, not the header, records which agents have touched the file." "${hits}" +fi + +# 4. A near miss. A line that is clearly meant to be the disclosure but is +# not spelled exactly right defeats every deduplication that follows it. +# +# Markdown is exempt from this one check. The contribution guide has to +# quote the accepted text and explain when it applies, so matching the bare +# phrase there reports prose rather than drift. A superseded form in a +# Markdown file is still caught by check 1. +mapfile -d '' -t SOURCES < <(printf '%s\0' "${FILES[@]}" | grep -zZv '\.md$' || true) +hits='' +if [ "${#SOURCES[@]}" -gt 0 ]; then + hits="$(grep -nIF 'AI assistance' -- "${SOURCES[@]}" 2>/dev/null \ + | grep -vF "${FIXED}" || true)" +fi +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: the text is not spelled exactly. + +The accepted text, character for character, is: + + ${FIXED}" "${hits}" +fi + +if [ "${status}" -eq 0 ]; then + echo "AI disclosure check passed." +fi + +exit "${status}" From 54e0badc68f0958f89651eb61cfe91568c7b73a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 17:56:08 -0400 Subject: [PATCH 11/14] Covered the fault-tolerant log guards left half-tested (#111) Three of the bounds added with GHSA-c8cj-fj9j-426v are only half covered. The replay guard that refuses a log recording less than the header, the FAT chain and the content header never takes its early return, and the enable guard is exercised only through its buffer-size condition. Three cases close them: a log recorded below the minimum the replay accepts, and enable over a log recording less than a reset one writes and over one recording more than the format allows. The last needs a buffer wider than the format maximum, because a smaller one is refused by the buffer condition first and the maximum condition is never reached; the test says so, since the obvious smaller buffer looks right and proves nothing. Reverting each of the three conditions on its own now fails the test. Nine configurations, zero warnings, zero errors. Assisted-by: Claude Code (Opus 5) --- ...ex_fault_tolerant_apply_logs_bounds_test.c | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c b/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c index 0b622a8..1b12a31 100644 --- a/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c +++ b/test/regression_test/filex_fault_tolerant_apply_logs_bounds_test.c @@ -38,6 +38,7 @@ void filex_fault_tolerant_apply_logs_bounds_test_application_define(void *fir #define DEMO_STACK_SIZE 4096 #define CACHE_SIZE 2048 #define FAULT_TOLERANT_SIZE FX_FAULT_TOLERANT_MINIMAL_BUFFER_SIZE +#define FAULT_TOLERANT_BUFFER (FX_FAULT_TOLERANT_MAXIMUM_LOG_FILE_SIZE + 1024) #define BYTES_PER_SECTOR 256 @@ -49,7 +50,7 @@ static UCHAR *cache_buffer; static UCHAR *fault_tolerant_buffer; #else static UCHAR cache_buffer[CACHE_SIZE]; -static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; +static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_BUFFER]; #endif static FX_MEDIA ram_disk; static UCHAR *pointer; @@ -178,6 +179,34 @@ UINT payload = 0; } +/* Stage one directory log entry, then record a log size that leaves no room for + the entries at all, and replay the log. */ +static UINT replay_dir_log_recording(ULONG recorded) +{ + +UINT status; +UINT payload = 0; + + status = media_prepare(); + if (status != FX_SUCCESS) + { + return(status); + } + + _fx_fault_tolerant_transaction_start(&ram_disk); + + status = _fx_fault_tolerant_add_dir_log(&ram_disk, log_data_sector(), 1, (UCHAR *)&payload, 1); + if (status != FX_SUCCESS) + { + return(status); + } + + ram_disk.fx_media_fault_tolerant_file_size = recorded; + + return(_fx_fault_tolerant_transaction_end(&ram_disk)); +} + + /* Stage one FAT log entry, then declare it shorter than a FAT log entry, and replay the log. */ static UINT replay_short_fat_log(VOID) @@ -307,7 +336,7 @@ void filex_fault_tolerant_apply_logs_bounds_test_application_define(void *fir cache_buffer = pointer; pointer += CACHE_SIZE; fault_tolerant_buffer = pointer; - pointer += FAULT_TOLERANT_SIZE; + pointer += FAULT_TOLERANT_BUFFER; #endif /* Initialize the FileX system. */ @@ -398,6 +427,31 @@ USHORT total_size; status = fx_media_close(&ram_disk); return_if_fail( status == FX_SUCCESS); + /* A log file that records less than the header, the FAT chain and the content + header together leaves no room for any entry, and the replay has to refuse it + before subtracting them from the recorded size. */ + status = replay_dir_log_recording(FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET + 2); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* A log recording less than a reset one writes is corrupt whatever the buffer. */ + status = enable_over_log_recording(FX_FAULT_TOLERANT_LOG_CONTENT_OFFSET - 1, FAULT_TOLERANT_SIZE); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + + /* So is one recording more than the format allows. The buffer here is larger + than the maximum on purpose: with a smaller one the buffer test refuses the + log first and this condition is never reached. */ + status = enable_over_log_recording(FX_FAULT_TOLERANT_MAXIMUM_LOG_FILE_SIZE + 1, FAULT_TOLERANT_BUFFER); + return_if_fail( status == FX_FILE_CORRUPT); + + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + /* Output successful. */ printf("SUCCESS!\n"); test_control_return(0); From 6156dce1bcaa46a04613de746a7cd62ce583df50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 18:50:59 -0400 Subject: [PATCH 12/14] Added Linux coverage and Win64 regression gates (#106) Added Linux coverage and Win64 regression gates FileX's regression workflow ran only on master, built four of the nine Linux coverage configurations, and never ran the Win64 simulator suite at all. It cloned ThreadX's default branch without pinning it, so a run validated FileX against whatever that branch happened to hold and never recorded which commit that was; an existing clone was reused without being refreshed, so a machine could go on building against a ThreadX from weeks earlier without saying so. Nothing enforced a coverage floor. The workflow now runs on dev as well as master, builds and tests all nine Linux coverage configurations, and adds a Win64 job. Coverage is collected from every configuration, merged, and gated at 99.9% of lines and 99.3% of branches. Pages deploys only after a successful master run. ThreadX is pinned by commit in three places that previously disagreed or floated: the revision file `run.sh` checks the working copy against, the reusable regression template, and the ThreadX the Win64 job checks out. All three name the ThreadX this release ships. `run.sh` fetches that exact revision and refuses to run against anything else, which closes the stale-clone case as well. GitHub actions are pinned by SHA, with Dependabot configured to move them. The branch floor sits one tenth of a percent below the measured rate rather than on it. The script compares whole tenths, so that is the nearest value that leaves any headroom at all, and a floor resting exactly on today's figure fails on the next commit that adds an uncovered branch rather than on a real regression. Most of what remains uncovered is in `fx_partition_offset_calculate.c`; filex#112 tracks covering it and raising the floor deliberately. Two MSVC build faults are fixed alongside, both found only once a Win64 build existed: the test RAM driver's header declared four buffers as arrays where its MSVC implementation allocates pointers, and five tests declared zero-length fault-tolerant buffers in configurations that disable the feature. Both now match the storage each configuration actually uses. The new partition-offset test takes two MISRA deviations. Rule 15.5, early returns, so each failure path can return a distinct code; and Rule 8.5, a local declaration of an internal function so the test can reach it directly. Nine Linux configurations and nine Win64 configurations, 138 tests each, all passing. Merged coverage 7651/7657 lines (99.92%) and 8228/8279 branches (99.38%). A forced failing test returned status 8; empty, missing and unfetchable coverage reports each failed as intended; installer retries stayed bounded. Actionlint, ShellCheck and whitespace checks pass. Assisted-by: Codex (GPT-6) Assisted-by: Claude Code (Opus 5) --- .github/dependabot.yml | 7 + .github/workflows/regression_test.yml | 93 +++++++- scripts/build.sh | 2 +- scripts/install.sh | 62 +++-- scripts/test.sh | 4 +- test/cmake/CMakeLists.txt | 13 +- test/cmake/coverage.sh | 139 ++++++++++- test/cmake/regression/CMakeLists.txt | 1 + test/cmake/run.sh | 33 ++- test/cmake/threadx-revision.txt | 1 + .../filex_directory_local_path_test.c | 32 ++- .../filex_partition_offset_extended_test.c | 224 ++++++++++++++++++ 12 files changed, 561 insertions(+), 50 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 test/cmake/threadx-revision.txt create mode 100644 test/regression_test/filex_partition_offset_extended_test.c diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ff41be5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + target-branch: dev diff --git a/.github/workflows/regression_test.yml b/.github/workflows/regression_test.yml index 5aab97d..613b2e3 100644 --- a/.github/workflows/regression_test.yml +++ b/.github/workflows/regression_test.yml @@ -1,25 +1,100 @@ -# This is a basic workflow that is manually triggered - name: regression_test -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch on: workflow_dispatch: push: - branches: [ master ] + branches: [dev, master] pull_request: - branches: [ master ] + branches: [dev, master] + +concurrency: + group: filex-regression-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "linux_job" run_tests: permissions: contents: read issues: read checks: write pull-requests: write + # GitHub validates the called workflow's skipped deploy job permissions. + # Its test job narrows its own token permissions. + pages: write + id-token: write + uses: eclipse-threadx/threadx/.github/workflows/regression_template.yml@80dfbe7797f4e03bb878f06b9557ddc4041cf212 + with: + coverage_name: merged + coverage_thresholds: '99 100' + skip_deploy: true + + win64: + runs-on: windows-2022 + timeout-minutes: 90 + permissions: + contents: read + steps: + - name: Check out FileX + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Check out ThreadX + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: eclipse-threadx/threadx + ref: 80dfbe7797f4e03bb878f06b9557ddc4041cf212 + path: threadx + - name: Build Win64 configurations + timeout-minutes: 35 + shell: pwsh + run: | + Start-Transcript -Path win64-build.log + try { + ./scripts/build_fx.ps1 -Arch win64 -Configuration all -ThreadXDir "$env:GITHUB_WORKSPACE/threadx" -BuildTimeoutSeconds 300 + } finally { + Stop-Transcript + } + - name: Test Win64 configurations + timeout-minutes: 50 + shell: pwsh + run: | + Start-Transcript -Path win64-test.log + try { + ./scripts/test_fx.ps1 -Arch win64 -Configuration all -Parallel 1 -TestTimeoutSeconds 600 + } finally { + Stop-Transcript + } + - name: Upload failure diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: win64-diagnostics + path: | + win64-build.log + win64-test.log + build/tests/win64/**/Testing/Temporary/* + build/tests/win64/**/CMakeFiles/CMakeConfigureLog.yaml + if-no-files-found: warn + + deploy: + if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' }} + needs: [run_tests, win64] + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: pages: write id-token: write - uses: eclipse-threadx/threadx/.github/workflows/regression_template.yml@master + steps: + - name: Download merged coverage + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: coverage_report-* + merge-multiple: true + path: coverage_report + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: coverage_report/merged + - name: Deploy coverage + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/scripts/build.sh b/scripts/build.sh index 736b73f..a7f75e3 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -10,4 +10,4 @@ # SPDX-License-Identifier: MIT ############################################################################## -$(dirname `realpath $0`)/../test/cmake/run.sh build all +"$(dirname "$(realpath "$0")")/../test/cmake/run.sh" build all diff --git a/scripts/install.sh b/scripts/install.sh index bcf4a0c..a5ddcb9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -10,30 +10,48 @@ # SPDX-License-Identifier: MIT ############################################################################## -# +set -euo pipefail + +retry() { + local attempt + for attempt in 1 2 3; do + if "$@"; then + return 0 + fi + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 5)) + fi + done + return 1 +} -# Remove large folder -rm -rf /opt/hostedtoolcache +apt_options=(-o Acquire::Retries=3 -o DPkg::Lock::Timeout=60) +if ! retry sudo timeout 150 apt-get "${apt_options[@]}" update; then + echo "Package index update failed; package installation will verify availability." >&2 +fi +retry sudo timeout 150 apt-get "${apt_options[@]}" install -y \ + cmake gcc-14 gcc-14-multilib git ninja-build python3-venv \ + unifdef p7zip-full tofrodos gawk -# Install necessary softwares for Ubuntu. +venv_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/filex-ci-venv" +python3 -m venv "$venv_dir" +retry timeout 120 "$venv_dir/bin/python" -m pip install \ + --retries 3 --timeout 30 gcovr==8.6 -sudo apt-get update -sudo apt-get install -y \ - gcc-multilib \ - git \ - g++ \ - python3-pip \ - ninja-build \ - unifdef \ - p7zip-full \ - tofrodos \ - gawk \ - software-properties-common +cc="${CC:-gcc-14}" +gcov="${GCOV:-gcov-14}" +cc_version=$("$cc" -dumpfullversion) +gcov_version=$("$gcov" --version | sed -n '1{s/.* \([0-9][0-9]*\.[0-9][0-9]*\(\.[0-9][0-9]*\)\?\).*/\1/p;}') +if [ -z "$gcov_version" ] || [ "$cc_version" != "$gcov_version" ]; then + echo "Compiler $cc and coverage tool $gcov have different versions." >&2 + exit 1 +fi -wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add - -CODENAME=$(lsb_release -c | cut -f2 -d':' | sed 's/\t//') -apt-add-repository "deb https://apt.kitware.com/ubuntu/ $CODENAME main" +if [ -n "${GITHUB_ENV:-}" ]; then + printf 'CC=%s\nGCOV=%s\n' "$cc" "$gcov" >> "$GITHUB_ENV" + printf '%s\n' "$venv_dir/bin" >> "$GITHUB_PATH" +fi -python3 -m pip install --upgrade pip -pip3 install gcovr==4.1 -pip install --upgrade cmake \ No newline at end of file +"$venv_dir/bin/gcovr" --version | head -1 +"$cc" --version | head -1 +"$gcov" --version | head -1 diff --git a/scripts/test.sh b/scripts/test.sh index 49fe8a3..cfe112d 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -10,4 +10,6 @@ # SPDX-License-Identifier: MIT ############################################################################## -$(dirname `realpath $0`)/../test/cmake/run.sh test all +export CTEST_PARALLEL_LEVEL="${CTEST_PARALLEL_LEVEL:-1}" +export CTEST_REPEAT_FAIL="${CTEST_REPEAT_FAIL:-1}" +"$(dirname "$(realpath "$0")")/../test/cmake/run.sh" test all diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt index b2168af..6b8bf86 100644 --- a/test/cmake/CMakeLists.txt +++ b/test/cmake/CMakeLists.txt @@ -12,6 +12,8 @@ endif() project(filex_test LANGUAGES C) +option(FX_COVERAGE "Instrument FileX in every Linux test configuration" OFF) + set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) @@ -100,15 +102,19 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../.. filex) add_subdirectory(regression) add_subdirectory(samples) -# Coverage instrumentation (GCC/Clang only — not supported by MSVC) -if(NOT MSVC AND CMAKE_BUILD_TYPE MATCHES ".*_coverage") +# TX_COVERAGE is supplied by the ThreadX regression runner. +if(NOT MSVC AND (FX_COVERAGE OR TX_COVERAGE OR CMAKE_BUILD_TYPE MATCHES ".*_coverage")) target_compile_options(filex PRIVATE -fprofile-arcs -ftest-coverage) target_link_options(filex PRIVATE -fprofile-arcs -ftest-coverage) endif() # Build ThreadX shared library for Linux if(NOT MSVC AND NOT FX_STANDALONE_ENABLE) - execute_process(COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) + execute_process(COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs + RESULT_VARIABLE threadx_build_result) + if(NOT threadx_build_result STREQUAL "0") + message(FATAL_ERROR "ThreadX dependency build failed: ${threadx_build_result}") + endif() add_custom_target(build_libs ALL COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) add_dependencies(filex build_libs) @@ -143,4 +149,3 @@ else() -Waggregate-return -Wfloat-equal) endif() - diff --git a/test/cmake/coverage.sh b/test/cmake/coverage.sh index ce81e77..07f9ab2 100755 --- a/test/cmake/coverage.sh +++ b/test/cmake/coverage.sh @@ -10,10 +10,139 @@ # SPDX-License-Identifier: MIT ############################################################################## +set -euo pipefail -set -e +cd "$(dirname "$0")" +repo_root=$(cd ../.. && pwd) +report_dir=coverage_report +configurations=( + default_build_coverage no_cache_build no_cache_standalone_build + fault_tolerant_build_coverage no_check_build no_cache_fault_tolerant_build + standalone_build_coverage standalone_fault_tolerant_build_coverage + standalone_no_cache_fault_tolerant_build +) -cd $(dirname $0) -mkdir -p coverage_report/$1 -gcovr --object-directory=build/$1/filex/CMakeFiles/filex.dir/common/src -r ../../common/src -e ".*driver.*" --xml-pretty --output coverage_report/$1.xml -gcovr --object-directory=build/$1/filex/CMakeFiles/filex.dir/common/src -r ../../common/src -e ".*driver.*" --html --html-details --output coverage_report/$1/index.html +check_report() { + python3 - "$@" <<'PY' +import json +import pathlib +import sys +import xml.etree.ElementTree as ET + +report = pathlib.Path(sys.argv[1]) +root = ET.parse(report).getroot() +classes = root.findall('.//class') +if int(root.get('lines-valid', '0')) == 0 or not classes: + raise SystemExit(f'{report}: report contains no measured FileX files') +if any(not item.get('filename', '').startswith('common/src/') for item in classes): + raise SystemExit(f'{report}: report contains a source outside common/src') +if len(sys.argv) > 2: + with open(sys.argv[2], encoding='utf-8') as stream: + data = json.load(stream) + if not data.get('files'): + raise SystemExit(f'{sys.argv[2]}: tracefile contains no measured files') + if any(not item.get('file', '').startswith('common/src/') for item in data['files']): + raise SystemExit(f'{sys.argv[2]}: tracefile contains a source outside common/src') +print(f"{report}: lines {root.get('lines-covered')}/{root.get('lines-valid')} " + f"({float(root.get('line-rate', '0')) * 100:.2f}%), branches " + f"{root.get('branches-covered')}/{root.get('branches-valid')} " + f"({float(root.get('branch-rate', '0')) * 100:.2f}%)") +PY +} + +check_merged_floor() { + python3 - "$1" <<'PY' +import sys +import xml.etree.ElementTree as ET + +root = ET.parse(sys.argv[1]).getroot() +for label, attribute, minimum in ( + ('line', 'lines', 999), + ('branch', 'branches', 993), +): + covered = int(root.get(f'{attribute}-covered', '0')) + valid = int(root.get(f'{attribute}-valid', '0')) + if valid == 0 or covered * 1000 < valid * minimum: + raise SystemExit( + f'{sys.argv[1]}: {label} coverage {covered}/{valid} ' + f'is below {minimum / 10:.1f}%' + ) +PY +} + +if [ "${1:-}" = --clean ]; then + if [ -d "$report_dir" ]; then + rm -r -- "$report_dir" + fi + if [ -d build ]; then + find build -type f -name '*.gcda' -delete + fi + exit 0 +fi + +if [ "${1:-}" = --merge ]; then + trace_args=() + for configuration in "${configurations[@]}"; do + base="$report_dir/per_configuration/$configuration" + for path in "$base.json" "$base.xml" "$base/index.html"; do + if [ ! -s "$path" ]; then + echo "Missing or empty coverage report: $path" >&2 + exit 1 + fi + done + check_report "$base.xml" "$base.json" + trace_args+=(--add-tracefile "$base.json") + done + + mkdir -p "$report_dir/merged" + gcovr -r "$repo_root" "${trace_args[@]}" --xml-pretty \ + --output "$report_dir/merged.xml" + gcovr -r "$repo_root" "${trace_args[@]}" --html --html-details \ + --output "$report_dir/merged/index.html" + check_report "$report_dir/merged.xml" + check_merged_floor "$report_dir/merged.xml" + exit 0 +fi + +configuration="${1:-}" +valid=0 +for item in "${configurations[@]}"; do + if [ "$configuration" = "$item" ]; then + valid=1 + break + fi +done +if [ "$valid" -ne 1 ]; then + echo "Unknown coverage configuration: $configuration" >&2 + exit 1 +fi + +cc_name=$(basename "${CC:-gcc}") +if [ -n "${GCOV:-}" ]; then + gcov="$GCOV" +elif [[ "$cc_name" = gcc* ]]; then + gcov="gcov${cc_name#gcc}" +else + gcov=gcov +fi +if ! command -v "$gcov" >/dev/null 2>&1; then + echo "Coverage tool $gcov is unavailable." >&2 + exit 1 +fi + +objects="$PWD/build/$configuration/filex/CMakeFiles/filex.dir/common/src" +if [ ! -d "$objects" ] || [ -z "$(find "$objects" -name '*.gcda' -print -quit)" ]; then + echo "No FileX coverage data for $configuration." >&2 + exit 1 +fi + +base="$report_dir/per_configuration/$configuration" +mkdir -p "$base" +# Regression tests use a separate driver with fault injection. +gcovr --gcov-executable "$gcov" -r "$repo_root" -f "$repo_root/common/src" \ + -e "$repo_root/common/src/fx_ram_driver.c" \ + "$objects" --json "$base.json" --xml-pretty --output "$base.xml" +gcovr --gcov-executable "$gcov" -r "$repo_root" -f "$repo_root/common/src" \ + -e "$repo_root/common/src/fx_ram_driver.c" \ + "$objects" --html --html-details --output "$base/index.html" +check_report "$base.xml" "$base.json" diff --git a/test/cmake/regression/CMakeLists.txt b/test/cmake/regression/CMakeLists.txt index 14145f3..e72989f 100644 --- a/test/cmake/regression/CMakeLists.txt +++ b/test/cmake/regression/CMakeLists.txt @@ -142,6 +142,7 @@ set(regression_test_cases ${SOURCE_DIR}/filex_file_write_notify_test.c ${SOURCE_DIR}/filex_file_write_available_cluster_test.c ${SOURCE_DIR}/filex_utility_test.c + ${SOURCE_DIR}/filex_partition_offset_extended_test.c ${SOURCE_DIR}/filex_utility_fat_flush_test.c) add_library(test_utility ${SOURCE_DIR}/fx_ram_driver_test.c diff --git a/test/cmake/run.sh b/test/cmake/run.sh index 4d7950a..e32ab61 100755 --- a/test/cmake/run.sh +++ b/test/cmake/run.sh @@ -10,10 +10,33 @@ # SPDX-License-Identifier: MIT ############################################################################## +set -euo pipefail -cd $(dirname $0) +cd "$(dirname "$0")" +revision=$(cat threadx-revision.txt) -# if threadx repo does not exist, clone it -[ -d threadx ] || git clone https://github.com/eclipse-threadx/threadx.git --depth 1 -[ -f .run.sh ] || ln -sf threadx/scripts/cmake_bootstrap.sh .run.sh -./.run.sh $* \ No newline at end of file +if [ ! -e threadx ]; then + git init -q threadx + git -C threadx remote add origin https://github.com/eclipse-threadx/threadx.git + timeout 180 git -C threadx fetch --depth 1 origin "$revision" + git -C threadx checkout -q --detach FETCH_HEAD +fi + +actual_revision=$(git -C threadx rev-parse HEAD) +if [ "$actual_revision" != "$revision" ]; then + echo "ThreadX checkout does not match threadx-revision.txt." >&2 + exit 1 +fi + +bootstrap=threadx/scripts/cmake_bootstrap.sh +if [ ! -f "$bootstrap" ]; then + echo "ThreadX bootstrap script is missing." >&2 + exit 1 +fi + +if [ "${1:-}" = test ] && [ "${2:-}" = all ] && [ "${TX_COVERAGE:-OFF}" = ON ]; then + ./coverage.sh --clean +fi + +ln -sfn "$bootstrap" .run.sh +exec ./.run.sh "$@" diff --git a/test/cmake/threadx-revision.txt b/test/cmake/threadx-revision.txt new file mode 100644 index 0000000..aaa9e4d --- /dev/null +++ b/test/cmake/threadx-revision.txt @@ -0,0 +1 @@ +80dfbe7797f4e03bb878f06b9557ddc4041cf212 diff --git a/test/regression_test/filex_directory_local_path_test.c b/test/regression_test/filex_directory_local_path_test.c index 71ea3db..99ba41a 100644 --- a/test/regression_test/filex_directory_local_path_test.c +++ b/test/regression_test/filex_directory_local_path_test.c @@ -9,6 +9,8 @@ /* SPDX-License-Identifier: MIT */ /***************************************************************************/ +/* Portions of this file were generated with AI assistance. */ + /* This FileX test concentrates on the local path operations. */ #ifndef FX_STANDALONE_ENABLE @@ -803,12 +805,36 @@ void filex_directory_local_path_application_define(void *first_unused_memory) #endif { +FX_MEDIA media = {0}; +FX_LOCAL_PATH local_path = {0}; +CHAR *path_name = FX_NULL; +UINT status; + FX_PARAMETER_NOT_USED(first_unused_memory); - /* Print out some test information banners. */ - printf("FileX Test: Directory local path test..............................N/A\n"); + /* Local paths are unavailable in standalone mode. */ + media.fx_media_id = FX_MEDIA_ID; + status = fx_directory_local_path_clear(&media); + if (status != FX_NOT_IMPLEMENTED) + { + test_control_return(1); + return; + } + status = fx_directory_local_path_get(&media, &path_name); + if (status != FX_NOT_IMPLEMENTED) + { + test_control_return(2); + return; + } + status = fx_directory_local_path_restore(&media, &local_path); + if (status != FX_NOT_IMPLEMENTED) + { + test_control_return(3); + return; + } - test_control_return(255); + printf("FileX Test: Directory local path test..............................SUCCESS\n"); + test_control_return(0); } #endif diff --git a/test/regression_test/filex_partition_offset_extended_test.c b/test/regression_test/filex_partition_offset_extended_test.c new file mode 100644 index 0000000..8087e75 --- /dev/null +++ b/test/regression_test/filex_partition_offset_extended_test.c @@ -0,0 +1,224 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Codex (GPT-6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +#include "fx_api.h" +#include + +#define TEST_SECTOR_SIZE 512u +#define TEST_PARTITION_OFFSET 446u +#define TEST_PARTITION_TYPE_OFFSET 4u +#define TEST_PARTITION_START_OFFSET 8u +#define TEST_PARTITION_SIZE_OFFSET 12u + +extern UINT _fx_partition_offset_calculate_extended(FX_MEDIA *media_ptr, void *partition_sector, + UINT partition, ULONG *partition_start, + ULONG *partition_size); +void test_control_return(UINT status); +void test_application_define(void *first_unused_memory); + +typedef enum TEST_DRIVER_MODE_ENUM +{ + TEST_DRIVER_SUCCESS, + TEST_DRIVER_READ_ERROR, + TEST_DRIVER_BAD_SIGNATURE, + TEST_DRIVER_EMPTY_PARTITION, + TEST_DRIVER_CHAIN +} TEST_DRIVER_MODE; + +static TEST_DRIVER_MODE test_driver_mode; + +/* Supply an extended boot record or a controlled read error. */ +static void test_partition_driver(FX_MEDIA *media_ptr) +{ +UCHAR *sector; + + if (test_driver_mode == TEST_DRIVER_READ_ERROR) + { + media_ptr->fx_media_driver_status = FX_IO_ERROR; + return; + } + + sector = (UCHAR *)media_ptr->fx_media_driver_buffer; + (void)memset(sector, 0, TEST_SECTOR_SIZE); + sector[510] = (UCHAR)0x55; + sector[511] = (UCHAR)0xAA; + if (test_driver_mode == TEST_DRIVER_BAD_SIGNATURE) + { + sector[510] = (UCHAR)0; + } + if (test_driver_mode != TEST_DRIVER_EMPTY_PARTITION) + { + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x0B; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)3; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_SIZE_OFFSET] = (UCHAR)9; + } + if (test_driver_mode == TEST_DRIVER_CHAIN) + { + if (media_ptr->fx_media_driver_logical_sector == (ULONG)7) + { + sector[TEST_PARTITION_OFFSET + 16u + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + sector[TEST_PARTITION_OFFSET + 16u + TEST_PARTITION_START_OFFSET] = (UCHAR)20; + } + else + { + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)4; + } + } + media_ptr->fx_media_driver_status = FX_SUCCESS; +} + +/* Verify boot records, primary partitions, and extended partition reads. */ +void test_application_define(void *first_unused_memory) +{ +FX_MEDIA media = {0}; +UCHAR sector[TEST_SECTOR_SIZE] = {0}; +ULONG start = 0; +ULONG size = 0; +UINT status; + + FX_PARAMETER_NOT_USED(first_unused_memory); + + sector[0] = (UCHAR)0xEB; + sector[2] = (UCHAR)0x90; + sector[0x16] = (UCHAR)1; + sector[0x13] = (UCHAR)0x34; + sector[0x14] = (UCHAR)0x12; + status = _fx_partition_offset_calculate_extended(&media, sector, 0, &start, &size); + if ((status != FX_SUCCESS) || (start != (ULONG)0) || (size != (ULONG)0x1234)) + { + test_control_return((UINT)1); + return; + } + + sector[0x13] = (UCHAR)0; + sector[0x14] = (UCHAR)0; + sector[0x20] = (UCHAR)0x78; + sector[0x21] = (UCHAR)0x56; + status = _fx_partition_offset_calculate_extended(&media, sector, 0, FX_NULL, &size); + if ((status != FX_SUCCESS) || (size != (ULONG)0x5678)) + { + test_control_return((UINT)2); + return; + } + + (void)memset(sector, 0, sizeof(sector)); + status = _fx_partition_offset_calculate_extended(&media, sector, 0, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)12); + return; + } + sector[510] = (UCHAR)0x55; + sector[511] = (UCHAR)0xAA; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x0B; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)7; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_SIZE_OFFSET] = (UCHAR)9; + status = _fx_partition_offset_calculate_extended(&media, sector, 0, &start, &size); + if ((status != FX_SUCCESS) || (start != (ULONG)7) || (size != (ULONG)9)) + { + test_control_return((UINT)3); + return; + } + status = _fx_partition_offset_calculate_extended(&media, sector, 0, FX_NULL, FX_NULL); + if (status != FX_SUCCESS) + { + test_control_return((UINT)4); + return; + } + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0; + status = _fx_partition_offset_calculate_extended(&media, sector, 0, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)13); + return; + } + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x0B; + status = _fx_partition_offset_calculate_extended(&media, sector, 17, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)5); + return; + } + status = _fx_partition_offset_calculate_extended(&media, sector, 4, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)6); + return; + } + + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + media.fx_media_driver_entry = test_partition_driver; + test_driver_mode = TEST_DRIVER_SUCCESS; + status = _fx_partition_offset_calculate_extended(&media, sector, 5, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)14); + return; + } + (void)memset(sector, 0, sizeof(sector)); + sector[510] = (UCHAR)0x55; + sector[511] = (UCHAR)0xAA; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)7; + status = _fx_partition_offset_calculate_extended(&media, sector, 4, &start, &size); + if ((status != FX_SUCCESS) || (start != (ULONG)10) || (size != (ULONG)9)) + { + test_control_return((UINT)7); + return; + } + + test_driver_mode = TEST_DRIVER_READ_ERROR; + (void)memset(sector, 0, sizeof(sector)); + sector[510] = (UCHAR)0x55; + sector[511] = (UCHAR)0xAA; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)7; + status = _fx_partition_offset_calculate_extended(&media, sector, 4, &start, &size); + if (status != FX_IO_ERROR) + { + test_control_return((UINT)8); + return; + } + test_driver_mode = TEST_DRIVER_BAD_SIGNATURE; + status = _fx_partition_offset_calculate_extended(&media, sector, 4, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)9); + return; + } + test_driver_mode = TEST_DRIVER_EMPTY_PARTITION; + sector[510] = (UCHAR)0x55; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)7; + status = _fx_partition_offset_calculate_extended(&media, sector, 4, &start, &size); + if (status != FX_NOT_FOUND) + { + test_control_return((UINT)10); + return; + } + + test_driver_mode = TEST_DRIVER_CHAIN; + sector[510] = (UCHAR)0x55; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_TYPE_OFFSET] = (UCHAR)0x05; + sector[TEST_PARTITION_OFFSET + TEST_PARTITION_START_OFFSET] = (UCHAR)7; + status = _fx_partition_offset_calculate_extended(&media, sector, 5, &start, &size); + if ((status != FX_SUCCESS) || (start != (ULONG)31) || (size != (ULONG)9)) + { + test_control_return((UINT)11); + return; + } + + test_control_return((UINT)0); +} From 71d939ac534254a6feb530a429a7408b5bbaa367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 20:02:11 -0400 Subject: [PATCH 13/14] Stopped the version pass skipping ports in silence (#113) The port stage matched a version only where "Version" was followed immediately by four dotted numbers. A port writing three numbers, or a stray letter before the first, was passed over and kept its old release, and the pass reported success either way. Four ports across the suite had gone stale that way, one of them for two consecutive releases. The pattern now accepts three or four numbers and tolerates a leading letter. A check follows it: any port header that names a release other than the one being prepared is listed and the pass stops, so a port the substitution cannot reach fails the release instead of shipping a version that misreports itself. Verified both ways against a FileX tree: with a port whose string the substitution cannot reach, the pass names the file and exits non-zero; with that string corrected, all four ports update and the pass completes. Assisted-by: Claude Code (Opus 5) --- scripts/prepare_release.sh | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 0124b47..2681940 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -145,15 +145,42 @@ PORT_FILES=$(printf "%s" "${PORT_FILES}" | grep -v '^[[:space:]]*$' || true) if [ -z "${PORT_FILES}" ]; then printf "Warning: No port header files found. Skipping port version string commit.\n" else + # A port advertises its release in the string the library reports at run time. + # The pattern accepts three or four dotted numbers and tolerates a stray + # letter before the first one, so a port that writes its version slightly + # differently is still reached. + VERSION_RE='Version[[:space:]]+[A-Za-z]?[0-9]+(\.[0-9]+)+[a-z]*' while IFS= read -r port_file; do - if [ -n "${port_file}" ] && grep -qE "Version [0-9]" "${port_file}" 2>/dev/null; then - sed -i -E "s/Version [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[a-z]*/Version ${VERSION}/g" "${port_file}" + if [ -n "${port_file}" ] && grep -qE "${VERSION_RE}" "${port_file}" 2>/dev/null; then + sed -i -E "s/${VERSION_RE}/Version ${VERSION}/g" "${port_file}" printf " Updated: %s\n" "${port_file#${REPO_ROOT}/}" fi done </dev/null || true) + [ -n "${advertised}" ] || continue + if ! printf "%s" "${advertised}" | grep -qF "${VERSION}"; then + STALE="${STALE} ${port_file#${REPO_ROOT}/} +" + fi + done <&2 + printf "Correct them so the substitution above reaches them, then re-run.\n" >&2 + exit 1 + fi + git -C "${REPO_ROOT}" add -u if git -C "${REPO_ROOT}" diff --cached --quiet; then printf "No port version string changes staged. Skipping commit.\n" From f480ba4cb4812e995687c6d16b312446eef777b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 20:03:43 -0400 Subject: [PATCH 14/14] Prepared the FileX 6.5.2.202603 release (#114) * Updated version number constants * Updated port version strings * Brought the Windows port version strings into the release pass The Win32 and Win64 ports report their version as "FileX Win64/Visual 6.5.1.202602", with no "Version" between the port name and the number. Every other FileX port writes "Version" there, and prepare_release.sh keys on it, both to decide a file needs rewriting and to perform the substitution. Neither port was touched by the version pass, so 6.5.2.202603 would have shipped with its two Windows ports reporting the previous release at runtime. Both strings now carry "Version" and read 6.5.2.202603, which also means the release script finds them from now on without needing to know these two ports exist. Verified: all four FileX ports now report 6.5.2.202603, and no source file outside a banner comment mentions the previous version. Assisted-by: Claude Code (Opus 5) --- common/inc/fx_api.h | 4 ++-- ports/generic/inc/fx_port.h | 2 +- ports/linux/gnu/inc/fx_port.h | 2 +- ports/win32/vs_2019/inc/fx_port.h | 2 +- ports/win64/vs_2022/inc/fx_port.h | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common/inc/fx_api.h b/common/inc/fx_api.h index 9c25594..42d759d 100644 --- a/common/inc/fx_api.h +++ b/common/inc/fx_api.h @@ -86,8 +86,8 @@ extern "C" { #define AZURE_RTOS_FILEX #define FILEX_MAJOR_VERSION 6 #define FILEX_MINOR_VERSION 5 -#define FILEX_PATCH_VERSION 1 -#define FILEX_BUILD_VERSION 202602 +#define FILEX_PATCH_VERSION 2 +#define FILEX_BUILD_VERSION 202603 #define FILEX_HOTFIX_VERSION ' ' /* Define the following symbols for backward compatibility */ diff --git a/ports/generic/inc/fx_port.h b/ports/generic/inc/fx_port.h index b3e8de7..73453ed 100644 --- a/ports/generic/inc/fx_port.h +++ b/ports/generic/inc/fx_port.h @@ -206,7 +206,7 @@ typedef unsigned long long ULONG64; #ifdef FX_SYSTEM_INIT CHAR _fx_version_id[] = - "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * FileX Generic Version 6.5.1.202602 *"; + "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * FileX Generic Version 6.5.2.202603 *"; #else extern CHAR _fx_version_id[]; #endif diff --git a/ports/linux/gnu/inc/fx_port.h b/ports/linux/gnu/inc/fx_port.h index fa420f4..170a3d2 100644 --- a/ports/linux/gnu/inc/fx_port.h +++ b/ports/linux/gnu/inc/fx_port.h @@ -331,7 +331,7 @@ extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, #ifdef FX_SYSTEM_INIT CHAR _fx_version_id[] = - "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * FileX Linux/GCC Version 6.5.1.202602 *"; + "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * FileX Linux/GCC Version 6.5.2.202603 *"; #else extern CHAR _fx_version_id[]; #endif diff --git a/ports/win32/vs_2019/inc/fx_port.h b/ports/win32/vs_2019/inc/fx_port.h index d0288df..c69ef76 100644 --- a/ports/win32/vs_2019/inc/fx_port.h +++ b/ports/win32/vs_2019/inc/fx_port.h @@ -361,7 +361,7 @@ extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, #ifdef FX_SYSTEM_INIT CHAR _fx_version_id[] = - "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win32/Visual 6.5.1.202602 *"; + "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win32/Visual Version 6.5.2.202603 *"; #else extern CHAR _fx_version_id[]; #endif diff --git a/ports/win64/vs_2022/inc/fx_port.h b/ports/win64/vs_2022/inc/fx_port.h index 68e9d8f..504a7b5 100644 --- a/ports/win64/vs_2022/inc/fx_port.h +++ b/ports/win64/vs_2022/inc/fx_port.h @@ -360,7 +360,7 @@ extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, #ifdef FX_SYSTEM_INIT CHAR _fx_version_id[] = - "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win64/Visual 6.5.1.202602 *"; + "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win64/Visual Version 6.5.2.202603 *"; #else extern CHAR _fx_version_id[]; #endif