Skip to content

Commit 9a2689a

Browse files
committed
fix(release): validate VHF pins portably
Normalize both sides of the VHF workflow policy checks so hosted Windows line endings cannot create false missing-gate failures. Replace unsupported fixed-count CMake regexes with explicit portable validators for release pins and sortable UUIDv7 ProductCodes, backed by focused malformed-input fixtures. Generated with [Codex](https://openai.com/codex/) Model: GPT 5.6-Sol High Orchestrator: GPT 5.6-Sol X-High
1 parent d6f5db3 commit 9a2689a

9 files changed

Lines changed: 253 additions & 11 deletions

.github/workflows/ci-windows.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,14 @@ jobs:
310310
shell: pwsh
311311
run: .\packaging\windows\virtual_gamepad_driver\tests\test_manifest_channel_contract.ps1
312312

313+
- name: Test VHF CMake release contract
314+
shell: pwsh
315+
run: cmake -P .\packaging\windows\virtual_gamepad_driver\tests\test_release_contract.cmake
316+
317+
- name: Test WiX sortable ProductCode contract
318+
shell: pwsh
319+
run: cmake -P .\packaging\windows\wix\tests\test_sortable_product_guid_contract.cmake
320+
313321
- name: Download verified VHF producer package
314322
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
315323
with:

cmake/packaging/windows.cmake

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,22 +290,37 @@ if(SUNSHINE_BUNDLE_VHF_GAMEPAD_DRIVER)
290290
endif()
291291
endforeach()
292292
unset(_vhf_gamepad_pin)
293-
if(NOT SUNSHINE_VHF_GAMEPAD_RELEASE_ASSET_SHA256 MATCHES "^[0-9a-fA-F]{64}$")
293+
sunshine_vhf_is_fixed_length_hex(
294+
_vhf_gamepad_pin_valid
295+
"${SUNSHINE_VHF_GAMEPAD_RELEASE_ASSET_SHA256}"
296+
64)
297+
if(NOT _vhf_gamepad_pin_valid)
294298
message(FATAL_ERROR "SUNSHINE_VHF_GAMEPAD_RELEASE_ASSET_SHA256 must be a SHA-256 value.")
295299
endif()
296-
if(NOT SUNSHINE_VHF_GAMEPAD_SOURCE_REVISION MATCHES "^[0-9a-fA-F]{40}$")
300+
sunshine_vhf_is_fixed_length_hex(
301+
_vhf_gamepad_pin_valid
302+
"${SUNSHINE_VHF_GAMEPAD_SOURCE_REVISION}"
303+
40)
304+
if(NOT _vhf_gamepad_pin_valid)
297305
message(FATAL_ERROR "SUNSHINE_VHF_GAMEPAD_SOURCE_REVISION must be a full commit SHA.")
298306
endif()
307+
unset(_vhf_gamepad_pin_valid)
299308
# Still validated when supplied, for the hand-signed package case.
300309
foreach(_vhf_gamepad_signer IN ITEMS
301310
SUNSHINE_VHF_GAMEPAD_CATALOG_SIGNER_THUMBPRINT
302311
SUNSHINE_VHF_GAMEPAD_DEVICE_SETUP_SIGNER_THUMBPRINT)
303-
if(NOT "${${_vhf_gamepad_signer}}" STREQUAL "" AND
304-
NOT "${${_vhf_gamepad_signer}}" MATCHES "^[0-9a-fA-F]{40}$")
305-
message(FATAL_ERROR
306-
"${_vhf_gamepad_signer} must be a 40-character SHA-1 thumbprint when set.")
312+
if(NOT "${${_vhf_gamepad_signer}}" STREQUAL "")
313+
sunshine_vhf_is_fixed_length_hex(
314+
_vhf_gamepad_signer_valid
315+
"${${_vhf_gamepad_signer}}"
316+
40)
317+
if(NOT _vhf_gamepad_signer_valid)
318+
message(FATAL_ERROR
319+
"${_vhf_gamepad_signer} must be a 40-character SHA-1 thumbprint when set.")
320+
endif()
307321
endif()
308322
endforeach()
323+
unset(_vhf_gamepad_signer_valid)
309324
unset(_vhf_gamepad_signer)
310325
endif()
311326

cmake/packaging/windows_virtual_gamepad_contract.cmake

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
include_guard(GLOBAL)
22

3+
function(sunshine_vhf_is_fixed_length_hex output_variable value expected_length)
4+
if(NOT expected_length MATCHES "^[0-9]+$" OR expected_length LESS 1)
5+
message(FATAL_ERROR "VHF hexadecimal validation requires a positive expected length.")
6+
endif()
7+
8+
string(LENGTH "${value}" actual_length)
9+
if(actual_length EQUAL expected_length AND value MATCHES "^[0-9a-fA-F]+$")
10+
set(${output_variable} TRUE PARENT_SCOPE)
11+
else()
12+
set(${output_variable} FALSE PARENT_SCOPE)
13+
endif()
14+
endfunction()
15+
316
# The virtual-gamepad package is independently versioned and released from
417
# Nonary/libvirtualgamepad. The application consumes its immutable archive; it
518
# must never build the UMDF driver while assembling a consumer MSI.

packaging/windows/virtual_gamepad_driver/tests/test_manifest_channel_contract.ps1

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,47 @@ try {
203203
Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue
204204
}
205205

206-
$workflowText = (Get-Content -LiteralPath $workflowPath -Raw).Replace("`r`n", "`n")
207206
$expectedGate = @'
208207
- name: Test VHF manifest channel contract
209208
shell: pwsh
210209
run: .\packaging\windows\virtual_gamepad_driver\tests\test_manifest_channel_contract.ps1
211210
'@
212-
if ($workflowText.IndexOf($expectedGate.TrimEnd(), [System.StringComparison]::Ordinal) -lt 0) {
211+
212+
function Test-WorkflowContainsManifestContractGate {
213+
param(
214+
[Parameter(Mandatory = $true)][string] $WorkflowText,
215+
[Parameter(Mandatory = $true)][string] $ExpectedGate
216+
)
217+
218+
$normalizedWorkflowText = $WorkflowText.Replace("`r`n", "`n").Replace("`r", "`n")
219+
$normalizedExpectedGate = $ExpectedGate.Replace("`r`n", "`n").Replace("`r", "`n").TrimEnd()
220+
return $normalizedWorkflowText.IndexOf(
221+
$normalizedExpectedGate,
222+
[System.StringComparison]::Ordinal) -ge 0
223+
}
224+
225+
$lfGateFixture = $expectedGate.Replace("`r`n", "`n").Replace("`r", "`n").TrimEnd()
226+
$crlfGateFixture = $lfGateFixture.Replace("`n", "`r`n")
227+
foreach ($validGateFixture in @($lfGateFixture, $crlfGateFixture)) {
228+
if (-not (Test-WorkflowContainsManifestContractGate `
229+
-WorkflowText $validGateFixture `
230+
-ExpectedGate $expectedGate)) {
231+
throw 'The manifest contract workflow gate rejected a valid newline style.'
232+
}
233+
}
234+
$missingGateFixture = $lfGateFixture.Replace(
235+
'.\packaging\windows\virtual_gamepad_driver\tests\test_manifest_channel_contract.ps1',
236+
'.\packaging\windows\virtual_gamepad_driver\tests\missing.ps1')
237+
if (Test-WorkflowContainsManifestContractGate `
238+
-WorkflowText $missingGateFixture `
239+
-ExpectedGate $expectedGate) {
240+
throw 'The manifest contract workflow gate accepted a missing policy test.'
241+
}
242+
243+
$workflowText = Get-Content -LiteralPath $workflowPath -Raw
244+
if (-not (Test-WorkflowContainsManifestContractGate `
245+
-WorkflowText $workflowText `
246+
-ExpectedGate $expectedGate)) {
213247
throw 'The ordinary/release Windows build does not run this manifest contract test.'
214248
}
215249
$packagingText = Get-Content -LiteralPath $windowsPackagingCmake -Raw

packaging/windows/virtual_gamepad_driver/tests/test_publisher_trust.ps1

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,47 @@ if ($trustOffset -lt 0 -or $pnpOffset -lt 0 -or $trustOffset -ge $pnpOffset) {
157157
throw 'Production publisher trust is not established before PnPUtil driver staging.'
158158
}
159159

160-
$workflowText = (Get-Content -LiteralPath $workflowPath -Raw).Replace("`r`n", "`n")
161160
$expectedGate = @'
162161
- name: Test VHF publisher trust policy
163162
shell: pwsh
164163
run: .\packaging\windows\virtual_gamepad_driver\tests\test_publisher_trust.ps1
165164
'@
166-
if ($workflowText.IndexOf($expectedGate.TrimEnd(), [System.StringComparison]::Ordinal) -lt 0) {
165+
166+
function Test-WorkflowContainsPublisherTrustGate {
167+
param(
168+
[Parameter(Mandatory = $true)][string] $WorkflowText,
169+
[Parameter(Mandatory = $true)][string] $ExpectedGate
170+
)
171+
172+
$normalizedWorkflowText = $WorkflowText.Replace("`r`n", "`n").Replace("`r", "`n")
173+
$normalizedExpectedGate = $ExpectedGate.Replace("`r`n", "`n").Replace("`r", "`n").TrimEnd()
174+
return $normalizedWorkflowText.IndexOf(
175+
$normalizedExpectedGate,
176+
[System.StringComparison]::Ordinal) -ge 0
177+
}
178+
179+
$lfGateFixture = $expectedGate.Replace("`r`n", "`n").Replace("`r", "`n").TrimEnd()
180+
$crlfGateFixture = $lfGateFixture.Replace("`n", "`r`n")
181+
foreach ($validGateFixture in @($lfGateFixture, $crlfGateFixture)) {
182+
if (-not (Test-WorkflowContainsPublisherTrustGate `
183+
-WorkflowText $validGateFixture `
184+
-ExpectedGate $expectedGate)) {
185+
throw 'The publisher trust workflow gate rejected a valid newline style.'
186+
}
187+
}
188+
$missingGateFixture = $lfGateFixture.Replace(
189+
'.\packaging\windows\virtual_gamepad_driver\tests\test_publisher_trust.ps1',
190+
'.\packaging\windows\virtual_gamepad_driver\tests\missing.ps1')
191+
if (Test-WorkflowContainsPublisherTrustGate `
192+
-WorkflowText $missingGateFixture `
193+
-ExpectedGate $expectedGate) {
194+
throw 'The publisher trust workflow gate accepted a missing policy test.'
195+
}
196+
197+
$workflowText = Get-Content -LiteralPath $workflowPath -Raw
198+
if (-not (Test-WorkflowContainsPublisherTrustGate `
199+
-WorkflowText $workflowText `
200+
-ExpectedGate $expectedGate)) {
167201
throw 'The ordinary/release Windows build does not run this publisher trust policy test.'
168202
}
169203

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
cmake_minimum_required(VERSION 3.25)
2+
3+
get_filename_component(repository_root "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE)
4+
include("${repository_root}/cmake/packaging/windows_virtual_gamepad_contract.cmake")
5+
6+
function(assert_fixed_hex_accepted case_name value expected_length)
7+
sunshine_vhf_is_fixed_length_hex(case_accepted "${value}" "${expected_length}")
8+
if(NOT case_accepted)
9+
message(FATAL_ERROR "${case_name}: valid fixed-length hexadecimal value was rejected.")
10+
endif()
11+
endfunction()
12+
13+
function(assert_fixed_hex_rejected case_name value expected_length)
14+
sunshine_vhf_is_fixed_length_hex(case_accepted "${value}" "${expected_length}")
15+
if(case_accepted)
16+
message(FATAL_ERROR "${case_name}: invalid fixed-length hexadecimal value was accepted.")
17+
endif()
18+
endfunction()
19+
20+
string(REPEAT "a" 64 sha256_lower)
21+
string(TOUPPER "${sha256_lower}" sha256_upper)
22+
assert_fixed_hex_accepted("lowercase SHA-256" "${sha256_lower}" 64)
23+
assert_fixed_hex_accepted("uppercase SHA-256" "${sha256_upper}" 64)
24+
25+
string(REPEAT "b" 40 sha1_lower)
26+
string(TOUPPER "${sha1_lower}" sha1_upper)
27+
assert_fixed_hex_accepted("lowercase SHA-1" "${sha1_lower}" 40)
28+
assert_fixed_hex_accepted("uppercase SHA-1" "${sha1_upper}" 40)
29+
30+
string(REPEAT "c" 63 sha256_short)
31+
string(REPEAT "d" 65 sha256_long)
32+
string(REPEAT "e" 63 sha256_nonhex_prefix)
33+
assert_fixed_hex_rejected("short SHA-256" "${sha256_short}" 64)
34+
assert_fixed_hex_rejected("long SHA-256" "${sha256_long}" 64)
35+
assert_fixed_hex_rejected("nonhex SHA-256" "${sha256_nonhex_prefix}g" 64)
36+
37+
string(REPEAT "1" 39 sha1_short)
38+
string(REPEAT "2" 41 sha1_long)
39+
string(REPEAT "3" 39 sha1_nonhex_prefix)
40+
assert_fixed_hex_rejected("short SHA-1" "${sha1_short}" 40)
41+
assert_fixed_hex_rejected("long SHA-1" "${sha1_long}" 40)
42+
assert_fixed_hex_rejected("nonhex SHA-1" "${sha1_nonhex_prefix}z" 40)
43+
44+
message(STATUS "VHF fixed-length hexadecimal release contract checks passed.")

packaging/windows/wix/generate_sortable_product_guid.cmake

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Keep the UpgradeCode stable, but generate a new ProductCode for every MSI so
33
# same-numeric-version prereleases can be ordered by canonical UUID text.
44

5+
include("${CMAKE_CURRENT_LIST_DIR}/sortable_product_guid_contract.cmake")
6+
57
function(_vibeshine_generate_sortable_product_guid _output_variable)
68
set(_guid "")
79

@@ -15,7 +17,8 @@ function(_vibeshine_generate_sortable_product_guid _output_variable)
1517
)
1618
endif()
1719

18-
if(NOT _guid MATCHES "^\\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-7[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}\\}$")
20+
vibeshine_is_sortable_product_guid(_guid_is_valid "${_guid}")
21+
if(NOT _guid_is_valid)
1922
# Fallback for non-PowerShell hosts. CMake only exposes seconds here, so
2023
# random bits keep ProductCodes unique if two packages are produced within
2124
# the same second; the leading timestamp still preserves sortable ordering.
@@ -49,6 +52,11 @@ function(_vibeshine_generate_sortable_product_guid _output_variable)
4952
set(_guid "{${_time_a}-${_time_b}-7${_rand_a}-8${_rand_b}-${_rand_c}}")
5053
endif()
5154

55+
vibeshine_is_sortable_product_guid(_guid_is_valid "${_guid}")
56+
if(NOT _guid_is_valid)
57+
message(FATAL_ERROR "Failed to generate a structurally valid sortable UUIDv7 ProductCode.")
58+
endif()
59+
5260
string(TOUPPER "${_guid}" _guid)
5361
set(${_output_variable} "${_guid}" PARENT_SCOPE)
5462
endfunction()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
include_guard(GLOBAL)
2+
3+
function(vibeshine_is_sortable_product_guid output_variable value)
4+
set(is_valid FALSE)
5+
string(LENGTH "${value}" value_length)
6+
if(value_length EQUAL 38)
7+
string(SUBSTRING "${value}" 0 1 opening_brace)
8+
string(SUBSTRING "${value}" 37 1 closing_brace)
9+
if(opening_brace STREQUAL "{" AND closing_brace STREQUAL "}")
10+
string(SUBSTRING "${value}" 1 36 uuid_text)
11+
string(REPLACE "-" ";" uuid_groups "${uuid_text}")
12+
list(LENGTH uuid_groups group_count)
13+
if(group_count EQUAL 5)
14+
list(GET uuid_groups 0 group_1)
15+
list(GET uuid_groups 1 group_2)
16+
list(GET uuid_groups 2 group_3)
17+
list(GET uuid_groups 3 group_4)
18+
list(GET uuid_groups 4 group_5)
19+
string(LENGTH "${group_1}" group_1_length)
20+
string(LENGTH "${group_2}" group_2_length)
21+
string(LENGTH "${group_3}" group_3_length)
22+
string(LENGTH "${group_4}" group_4_length)
23+
string(LENGTH "${group_5}" group_5_length)
24+
if(group_1_length EQUAL 8 AND
25+
group_2_length EQUAL 4 AND
26+
group_3_length EQUAL 4 AND
27+
group_4_length EQUAL 4 AND
28+
group_5_length EQUAL 12)
29+
string(CONCAT uuid_hex
30+
"${group_1}" "${group_2}" "${group_3}" "${group_4}" "${group_5}")
31+
string(SUBSTRING "${group_3}" 0 1 version_nibble)
32+
string(SUBSTRING "${group_4}" 0 1 variant_nibble)
33+
if(uuid_hex MATCHES "^[0-9A-Fa-f]+$" AND
34+
version_nibble STREQUAL "7" AND
35+
variant_nibble MATCHES "^[89ABab]$")
36+
set(is_valid TRUE)
37+
endif()
38+
endif()
39+
endif()
40+
endif()
41+
endif()
42+
set(${output_variable} "${is_valid}" PARENT_SCOPE)
43+
endfunction()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
cmake_minimum_required(VERSION 3.25)
2+
3+
get_filename_component(wix_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
4+
include("${wix_root}/sortable_product_guid_contract.cmake")
5+
6+
function(assert_guid_accepted case_name value)
7+
vibeshine_is_sortable_product_guid(case_accepted "${value}")
8+
if(NOT case_accepted)
9+
message(FATAL_ERROR "${case_name}: valid sortable UUIDv7 value was rejected.")
10+
endif()
11+
endfunction()
12+
13+
function(assert_guid_rejected case_name value)
14+
vibeshine_is_sortable_product_guid(case_accepted "${value}")
15+
if(case_accepted)
16+
message(FATAL_ERROR "${case_name}: malformed sortable UUIDv7 value was accepted.")
17+
endif()
18+
endfunction()
19+
20+
assert_guid_accepted(
21+
"canonical UUIDv7"
22+
"{018F0C11-1111-7ABC-8DEF-0123456789AB}")
23+
assert_guid_accepted(
24+
"lowercase UUIDv7"
25+
"{018f0c11-1111-7abc-abcd-0123456789ab}")
26+
27+
assert_guid_rejected(
28+
"malformed group lengths"
29+
"{018F0C1-11111-7ABC-8DEF-0123456789AB}")
30+
assert_guid_rejected(
31+
"short overall value"
32+
"{018F0C11-1111-7ABC-8DEF-0123456789A}")
33+
assert_guid_rejected(
34+
"nonhex value"
35+
"{018F0C1G-1111-7ABC-8DEF-0123456789AB}")
36+
assert_guid_rejected(
37+
"non-v7 version"
38+
"{018F0C11-1111-6ABC-8DEF-0123456789AB}")
39+
assert_guid_rejected(
40+
"invalid UUID variant"
41+
"{018F0C11-1111-7ABC-7DEF-0123456789AB}")
42+
43+
message(STATUS "Sortable UUIDv7 ProductCode contract checks passed.")

0 commit comments

Comments
 (0)