From 43fcdd6a7236835e1af8cb553f20a4d83ab7613d Mon Sep 17 00:00:00 2001 From: Fei-Away <100042407+Fei-Away@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:18:01 +0800 Subject: [PATCH] Fix Windows config rewrite with multiline arrays --- macos/CHANGELOG.md | 4 + macos/VERSION | 2 +- macos/package.json | 2 +- macos/scripts/common-macos.sh | 2 +- macos/scripts/injector.mjs | 2 +- macos/tests/run-tests.sh | 4 +- windows/CHANGELOG.md | 1 + windows/VERSION | 2 +- windows/scripts/config-utf8.ps1 | 176 +++++++++++++++--- windows/scripts/injector.mjs | 2 +- .../tests/injector-window-readiness.test.mjs | 2 +- windows/tests/run-tests.ps1 | 24 ++- .../start-verified-skin-preserved.tests.ps1 | 2 +- 13 files changed, 188 insertions(+), 37 deletions(-) diff --git a/macos/CHANGELOG.md b/macos/CHANGELOG.md index 9c8363e3..7b29c653 100644 --- a/macos/CHANGELOG.md +++ b/macos/CHANGELOG.md @@ -8,6 +8,10 @@ - 修复显式浅色、宽幅 artwork 的 `taskMode: "full"` 任务页 Markdown 继承原生深色壳白色前景的问题(#309)。完整模式现与 ambient/banner 一样强制使用主题文本色,并保留浅色壳对比阴影。 - 修复社区主题 ZIP 导入拒绝 `backdrop-filter: blur(var(--ds-theme-surface-blur))` 的问题(#312)。Safe CSS 仍只允许 `none`、0-20px blur 或注册的主题 blur 变量,不放宽到任意 filter 函数。 +### 内部 + +- 同步 v1.5.8 版本号以发布 Windows 安装器补丁;macOS 运行时代码相对 v1.5.7 无行为变化。 + ## 1.5.6 — 2026-07-26 ### 安全 diff --git a/macos/VERSION b/macos/VERSION index f01291b8..1cc9c180 100644 --- a/macos/VERSION +++ b/macos/VERSION @@ -1 +1 @@ -1.5.7 +1.5.8 diff --git a/macos/package.json b/macos/package.json index 5e1fc333..6ddf7e02 100644 --- a/macos/package.json +++ b/macos/package.json @@ -1,6 +1,6 @@ { "name": "codex-dream-skin-studio", - "version": "1.5.7", + "version": "1.5.8", "private": true, "type": "module", "scripts": { diff --git a/macos/scripts/common-macos.sh b/macos/scripts/common-macos.sh index e815649b..7a50b6b5 100755 --- a/macos/scripts/common-macos.sh +++ b/macos/scripts/common-macos.sh @@ -29,7 +29,7 @@ CODEX_APP_JOB_LABEL="com.openai.codex-dream-skin-studio.app" INJECTOR_JOB_LABEL="com.openai.codex-dream-skin-studio.injector" EXPECTED_CODEX_TEAM_ID="2DC432GLL2" EXPECTED_CODEX_REQUIREMENT="anchor apple generic and certificate leaf[subject.OU] = \"$EXPECTED_CODEX_TEAM_ID\"" -SKIN_VERSION="1.5.7" +SKIN_VERSION="1.5.8" DREAM_SKIN_VALIDATED_RUNTIME_PID="" DREAM_SKIN_VALIDATED_RUNTIME_BUNDLE="" DREAM_SKIN_VALIDATED_RUNTIME_EXE="" diff --git a/macos/scripts/injector.mjs b/macos/scripts/injector.mjs index 04d51fc7..552019df 100644 --- a/macos/scripts/injector.mjs +++ b/macos/scripts/injector.mjs @@ -43,7 +43,7 @@ const stableTestidLiteral = (testid) => { } return JSON.stringify(`[data-testid="${testid}"]`); }; -const SKIN_VERSION = "1.5.7"; +const SKIN_VERSION = "1.5.8"; const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]); const CDP_ID_PATTERN = /^[A-Za-z0-9._-]{1,200}$/; const MAX_ART_BYTES = 10 * 1024 * 1024; diff --git a/macos/tests/run-tests.sh b/macos/tests/run-tests.sh index 79cc8c12..f51461a9 100755 --- a/macos/tests/run-tests.sh +++ b/macos/tests/run-tests.sh @@ -86,7 +86,7 @@ UPDATE_JSON="$({ })" "$NODE" -e ' const value = JSON.parse(process.argv[1]); - if (value.currentVersion !== "v1.5.7" || value.latestVersion !== "v9.8.7") process.exit(1); + if (value.currentVersion !== "v1.5.8" || value.latestVersion !== "v9.8.7") process.exit(1); if (!value.updateAvailable) process.exit(1); if (value.releaseUrl !== "https://github.com/Fei-Away/Codex-Dream-Skin/releases/latest") process.exit(1); ' "$UPDATE_JSON" @@ -1071,7 +1071,7 @@ CRLF_BACKUP="$TMP/config-crlf-backup.json" "$NODE" "$ROOT/scripts/theme-config.mjs" restore "$CRLF_CONFIG" "$CRLF_BACKUP" >/dev/null /usr/bin/cmp -s "$CRLF_CONFIG" "$TMP/original-crlf.toml" -/usr/bin/env -u HOME /bin/bash -c '. "$1/scripts/common-macos.sh"; [ -n "$HOME" ] && [ "$SKIN_VERSION" = "1.5.7" ]' _ "$ROOT" +/usr/bin/env -u HOME /bin/bash -c '. "$1/scripts/common-macos.sh"; [ -n "$HOME" ] && [ "$SKIN_VERSION" = "1.5.8" ]' _ "$ROOT" if [ "${CODEX_DREAM_SKIN_SKIP_DOCTOR:-0}" = "1" ]; then printf 'SKIP: Doctor requires an installed, signed Codex app.\n' DOCTOR_RESULT="skipped" diff --git a/windows/CHANGELOG.md b/windows/CHANGELOG.md index d82703bc..b6a07e48 100644 --- a/windows/CHANGELOG.md +++ b/windows/CHANGELOG.md @@ -4,6 +4,7 @@ ### 修复 +- 修复安装器遇到 Codex `config.toml` 中合法多行数组时直接拒绝写入的问题(#313)。配置编辑器现在按 TOML 结构扫描 table header,安全跨过普通多行数组并保持原字节风格;未闭合数组、括号不匹配,以及 Dream Skin 需要改写的目标 key 自身为多行值时仍会在写入前 fail-closed。 - 修复 v1.5.6 安装器在部分 Windows 10/11 环境中校验自带 Node.js 签名时,PowerShell 自动加载 `Microsoft.PowerShell.Security` / `Get-AuthenticodeSignature` 失败而中止安装的问题(#313、#314)。签名校验现在会在执行 `node.exe` 前显式加载安全模块,并在模块名加载失败时回退到 `$PSHOME` 下的系统模块清单路径;签名状态和发行者校验仍保持 fail-closed。 - 修复社区主题一键换肤和 ZIP 导入拒绝 `backdrop-filter: blur(var(--ds-theme-surface-blur))` 的问题(#307、#312)。Safe CSS 仍只允许 `none`、0-20px blur 或注册的主题 blur 变量,不放宽到任意 filter 函数。 - 修复 Codex Desktop 26.721.x 首页在 `home-icon` 延迟渲染时被误判为注入校验失败的问题(#307)。Windows 校验现在与 macOS 一样复用已由首页内容信号解析出的 `[role="main"]` 容器;严格的 `home-icon` 路径仍优先,旧版行为不变。 diff --git a/windows/VERSION b/windows/VERSION index f01291b8..1cc9c180 100644 --- a/windows/VERSION +++ b/windows/VERSION @@ -1 +1 @@ -1.5.7 +1.5.8 diff --git a/windows/scripts/config-utf8.ps1 b/windows/scripts/config-utf8.ps1 index 946755aa..75b1e3ab 100644 --- a/windows/scripts/config-utf8.ps1 +++ b/windows/scripts/config-utf8.ps1 @@ -188,17 +188,119 @@ function Get-DreamSkinTomlArrayBracketBalance { return $balance } +function Get-DreamSkinTomlLineStructure { + param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Line) + + $builder = [System.Text.StringBuilder]::new() + $quote = $null + $escaped = $false + for ($index = 0; $index -lt $Line.Length; $index++) { + $character = $Line[$index] + if ($quote -eq '"') { + if ($escaped) { $escaped = $false; continue } + if ($character -eq '\') { $escaped = $true; continue } + if ($character -eq $quote) { $quote = $null } + continue + } + if ($quote -eq "'") { + if ($character -eq $quote) { $quote = $null } + continue + } + if ($character -eq '"' -or $character -eq "'") { + $quote = $character + continue + } + if ($character -eq '#') { break } + [void]$builder.Append($character) + } + return $builder.ToString() +} + +function Test-DreamSkinTomlTableHeaderStructure { + param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Structure) + + $value = $Structure.Trim() + if ($value.StartsWith('[[')) { + return $value.EndsWith(']]') -and + -not $value.Substring(2, $value.Length - 4).Contains('[') -and + -not $value.Substring(2, $value.Length - 4).Contains(']') + } + return $value.StartsWith('[') -and + -not $value.StartsWith('[[') -and + $value.EndsWith(']') -and + -not $value.Substring(1, $value.Length - 2).Contains('[') -and + -not $value.Substring(1, $value.Length - 2).Contains(']') +} + +function Update-DreamSkinTomlArrayDepth { + param( + [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Structure, + [Parameter(Mandatory = $true)][int]$InitialDepth + ) + + $depth = $InitialDepth + for ($index = 0; $index -lt $Structure.Length; $index++) { + $character = $Structure[$index] + if ($character -eq '[') { $depth++ } + if ($character -eq ']') { $depth-- } + if ($depth -lt 0) { + throw 'Refusing to rewrite TOML containing an unmatched array bracket.' + } + } + return $depth +} + +function Get-DreamSkinTomlTableHeaders { + param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content) + + $headers = @() + $offset = 0 + $arrayDepth = 0 + $lines = [regex]::Matches($Content, '[^\n]*\n|[^\n]+$') + $desktopToken = Get-DreamSkinTomlKeyTokenPattern -Key 'desktop' + foreach ($lineMatch in $lines) { + $line = $lineMatch.Value + $structure = (Get-DreamSkinTomlLineStructure -Line $line).Trim() + if ($arrayDepth -eq 0 -and (Test-DreamSkinTomlTableHeaderStructure -Structure $structure)) { + $headers += [pscustomobject]@{ + Index = $offset + BodyStart = $offset + $line.Length + Line = $line + IsDesktop = [regex]::IsMatch( + $line, + "^[\t ]*\[[\t ]*$desktopToken[\t ]*\][\t ]*(?:#[^\r\n]*)?(?:\r?\n)?$" + ) + IsDesktopArray = [regex]::IsMatch( + $line, + "^[\t ]*\[\[[\t ]*$desktopToken[\t ]*(?:\]\]|\.)" + ) + } + } else { + $assignment = $structure.IndexOf('=') + if ($arrayDepth -eq 0 -and $assignment -lt 0) { + if ($structure.Contains('[') -or $structure.Contains(']')) { + throw 'Refusing to rewrite malformed TOML array syntax.' + } + } else { + $expression = if ($arrayDepth -gt 0) { $structure } else { $structure.Substring($assignment + 1) } + $arrayDepth = Update-DreamSkinTomlArrayDepth -Structure $expression -InitialDepth $arrayDepth + } + } + $offset += $line.Length + } + if ($arrayDepth -ne 0) { + throw 'Refusing to rewrite TOML containing an unterminated array.' + } + return @($headers) +} + function Assert-DreamSkinTomlLineEditingSafe { param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content) if ($Content.Contains('"""') -or $Content.Contains("'''")) { throw 'Refusing to rewrite TOML containing multiline strings; use single-line values before installing Dream Skin.' } - foreach ($match in [regex]::Matches($Content, '(?m)^[^\r\n]*=[\t ]*\[[^\r\n]*\r?$')) { - if ((Get-DreamSkinTomlArrayBracketBalance -Line $match.Value) -ne 0) { - throw 'Refusing to rewrite TOML containing multiline arrays; use single-line arrays before installing Dream Skin.' - } - } + $null = Get-DreamSkinTomlTableHeaders -Content $Content $probe = ConvertTo-DreamSkinTomlAsciiEscapeProbe -Value $Content if ($probe -cne $Content) { @@ -225,23 +327,27 @@ function Test-DreamSkinDesktopNestedTable { $desktopToken = Get-DreamSkinTomlKeyTokenPattern -Key 'desktop' $keyToken = Get-DreamSkinTomlKeyTokenPattern -Key $Key - return [regex]::IsMatch( - $Content, - "(?m)^[\t ]*\[[\t ]*$desktopToken[\t ]*\.[\t ]*$keyToken[\t ]*(?:\]|\.)" - ) + foreach ($header in @(Get-DreamSkinTomlTableHeaders -Content $Content)) { + if ([regex]::IsMatch( + $header.Line, + "^[\t ]*\[[\t ]*$desktopToken[\t ]*\.[\t ]*$keyToken[\t ]*(?:\]|\.)" + )) { + return $true + } + } + return $false } function Assert-DreamSkinDesktopShapeSupported { param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content) Assert-DreamSkinTomlLineEditingSafe -Content $Content - $sectionPattern = Get-DreamSkinDesktopSectionPattern - if ([regex]::Matches($Content, $sectionPattern).Count -gt 1) { + $headers = @(Get-DreamSkinTomlTableHeaders -Content $Content) + if (@($headers | Where-Object { $_.IsDesktop }).Count -gt 1) { throw 'Refusing to rewrite multiple equivalent [desktop] tables.' } - $desktopToken = Get-DreamSkinTomlKeyTokenPattern -Key 'desktop' - if ([regex]::IsMatch($Content, "(?m)^[\t ]*\[\[[\t ]*$desktopToken[\t ]*(?:\]\]|\.)")) { + if (@($headers | Where-Object { $_.IsDesktopArray }).Count -gt 0) { throw 'Refusing to rewrite a config that represents desktop as an array of tables.' } foreach ($key in @('appearanceTheme', 'appearanceLightCodeThemeId')) { @@ -250,8 +356,9 @@ function Assert-DreamSkinDesktopShapeSupported { } } - $firstTable = [regex]::Match($Content, '(?m)^[\t ]*\[\[?') - $rootContent = if ($firstTable.Success) { $Content.Substring(0, $firstTable.Index) } else { $Content } + $desktopToken = Get-DreamSkinTomlKeyTokenPattern -Key 'desktop' + $firstTable = @($headers)[0] + $rootContent = if ($null -ne $firstTable) { $Content.Substring(0, $firstTable.Index) } else { $Content } if ([regex]::IsMatch($rootContent, "(?m)^[\t ]*$desktopToken[\t ]*(?:\.|=)")) { throw 'Refusing to rewrite root dotted or inline desktop keys; normalize them to a [desktop] table first.' } @@ -281,14 +388,24 @@ function Assert-DreamSkinDesktopShapeSupported { function Get-DreamSkinDesktopSection { param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content) - $match = [regex]::Match($Content, (Get-DreamSkinDesktopSectionPattern)) - if (-not $match.Success) { return $null } + $headers = @(Get-DreamSkinTomlTableHeaders -Content $Content) + $desktopHeaders = @($headers | Where-Object { $_.IsDesktop }) + if ($desktopHeaders.Count -eq 0) { return $null } + if ($desktopHeaders.Count -gt 1) { throw 'Refusing to rewrite multiple equivalent [desktop] tables.' } + $desktopHeader = $desktopHeaders[0] + $headerIndex = [array]::IndexOf($headers, $desktopHeader) + $bodyEnd = if ($headerIndex -ge 0 -and $headerIndex + 1 -lt $headers.Count) { + $headers[$headerIndex + 1].Index + } else { + $Content.Length + } + $bodyLength = $bodyEnd - $desktopHeader.BodyStart return [pscustomobject]@{ - Body = $match.Groups['body'].Value - BodyStart = $match.Groups['body'].Index - BodyLength = $match.Groups['body'].Length - SectionStart = $match.Index - SectionLength = $match.Length + Body = $Content.Substring($desktopHeader.BodyStart, $bodyLength) + BodyStart = $desktopHeader.BodyStart + BodyLength = $bodyLength + SectionStart = $desktopHeader.Index + SectionLength = $bodyEnd - $desktopHeader.Index } } @@ -312,11 +429,17 @@ function Set-DreamSkinSectionSetting { ) $keyToken = Get-DreamSkinTomlKeyTokenPattern -Key $Key - $pattern = "(?m)^[\t ]*$keyToken[\t ]*=[^\r\n]*(?:\r?\n|(?=\z))" - $matcher = [regex]::new($pattern) - if ($matcher.Matches($Body).Count -gt 1) { + $lineMatches = [regex]::Matches($Body, "(?m)^[\t ]*$keyToken[\t ]*=.*$") + if ($lineMatches.Count -gt 1) { throw "Refusing to rewrite duplicate '$Key' entries in the [desktop] section." } + foreach ($lineMatch in $lineMatches) { + if ((Get-DreamSkinTomlArrayBracketBalance -Line $lineMatch.Value) -ne 0) { + throw "Refusing to rewrite multiline '$Key' settings in the [desktop] section." + } + } + $pattern = "(?m)^[\t ]*$keyToken[\t ]*=[^\r\n]*(?:\r?\n|(?=\z))" + $matcher = [regex]::new($pattern) if ($null -eq $Line) { return $matcher.Replace($Body, '', 1) } $normalizedLine = $Line.TrimEnd("`r", "`n") + $NewLine if ($matcher.IsMatch($Body)) { @@ -336,6 +459,9 @@ function Get-DreamSkinSectionSettingLine { $matches = [regex]::Matches($Body, "(?m)^[\t ]*$keyToken[\t ]*=.*$") if ($matches.Count -gt 1) { throw "Refusing to inspect duplicate '$Key' entries in the [desktop] section." } if ($matches.Count -eq 0) { return $null } + if ((Get-DreamSkinTomlArrayBracketBalance -Line $matches[0].Value) -ne 0) { + throw "Refusing to inspect multiline '$Key' settings in the [desktop] section." + } return $matches[0].Value.Trim() } diff --git a/windows/scripts/injector.mjs b/windows/scripts/injector.mjs index 5bdabf5f..b31121dd 100644 --- a/windows/scripts/injector.mjs +++ b/windows/scripts/injector.mjs @@ -39,7 +39,7 @@ const stableTestidLiteral = (testid) => { } return JSON.stringify(`[data-testid="${testid}"]`); }; -const SKIN_VERSION = "1.5.7"; +const SKIN_VERSION = "1.5.8"; const MAX_ART_BYTES = 10 * 1024 * 1024; const MAX_SAFE_CSS_BYTES = 256 * 1024; const STRONG_THEME_AUDIT_MS = 30000; diff --git a/windows/tests/injector-window-readiness.test.mjs b/windows/tests/injector-window-readiness.test.mjs index 0d0ad3b9..a537969b 100644 --- a/windows/tests/injector-window-readiness.test.mjs +++ b/windows/tests/injector-window-readiness.test.mjs @@ -89,7 +89,7 @@ function makeDomFixture({ }; const window = { __CODEX_DREAM_SKIN_STATE__: { - version: "1.5.7", + version: "1.5.8", themeId: "fixture-theme", revision: "fixture-revision", styleMode: "style", diff --git a/windows/tests/run-tests.ps1 b/windows/tests/run-tests.ps1 index e25cf5b1..095b648d 100644 --- a/windows/tests/run-tests.ps1 +++ b/windows/tests/run-tests.ps1 @@ -537,6 +537,26 @@ try { throw 'A safe single-line array containing bracket text was changed or rejected.' } + $multilineArrayPath = Join-Path $temporaryRoot 'config-multiline-array.toml' + $multilineArrayBackup = Join-Path $temporaryRoot 'config-multiline-array.before.toml' + $multilineArrayOriginal = "model = `"gpt-5`"`r`nfeatures = [`r`n `"hash # stays inside the string`",`r`n `"brackets [stay] inside the string`", # ignored comment brackets []`r`n [`"nested`", `"array`"],`r`n]`r`n`r`n[desktop]`r`nlayout = [`r`n [`"one`", `"two`"], # unrelated desktop array`r`n [`"three`", `"[desktop]`"],`r`n]`r`nappearanceTheme = `"system`"`r`nappearanceLightCodeThemeId = `"github-light`"`r`nkeepMe = true`r`n`r`n[mcp_servers.example]`r`nargs = [`r`n `"--flag`",`r`n `"value#with-hash`",`r`n]`r`n" + [System.IO.File]::WriteAllText($multilineArrayPath, $multilineArrayOriginal, $utf8NoBom) + Install-DreamSkinBaseTheme -ConfigPath $multilineArrayPath -BackupPath $multilineArrayBackup -AppearanceTheme 'dark' + $multilineArrayInstalled = Read-DreamSkinUtf8File -Path $multilineArrayPath + if (-not $multilineArrayInstalled.Contains('layout = [')) { + throw 'Install removed an unrelated multiline array from the [desktop] table.' + } + if (-not $multilineArrayInstalled.Contains('[mcp_servers.example]')) { + throw 'Install lost a following TOML table after a multiline array.' + } + if (-not $multilineArrayInstalled.Contains('appearanceTheme = "dark"')) { + throw 'Install did not update appearanceTheme beside multiline arrays.' + } + Restore-DreamSkinBaseTheme -ConfigPath $multilineArrayPath -BackupPath $multilineArrayBackup + if ((Read-DreamSkinUtf8File -Path $multilineArrayPath) -cne $multilineArrayOriginal) { + throw 'Multiline arrays were not preserved exactly through install and restore.' + } + foreach ($unsupported in @( 'desktop.appearanceTheme = "system"', 'desktop = { appearanceTheme = "system" }', @@ -549,8 +569,8 @@ try { '["desk\u0074op"]', "note = `"`"`"fake`r`n[desktop]`r`nappearanceTheme = `"dark`"`r`n`"`"`"", "[desktop]`r`nappearanceTheme = [`r`n `"light`"`r`n]", - "[desktop]`r`nlayout = [`r`n [1, 2],`r`n [3, 4],`r`n]`r`nappearanceTheme = `"dark`"", - "[desktop]`r`nlayout = [`"]`",`r`n [`"[`", `"]`"],`r`n]`r`nappearanceTheme = `"dark`"" + "features = ]`r`n`r`n[desktop]`r`nappearanceTheme = `"system`"", + "features = [`r`n `"one`"`r`n`r`n[desktop]`r`nappearanceTheme = `"system`"" )) { $unsupportedPath = Join-Path $temporaryRoot ("unsupported-$([guid]::NewGuid().ToString('N')).toml") $unsupportedBackup = "$unsupportedPath.before" diff --git a/windows/tests/start-verified-skin-preserved.tests.ps1 b/windows/tests/start-verified-skin-preserved.tests.ps1 index faffa9af..c3ce10a7 100644 --- a/windows/tests/start-verified-skin-preserved.tests.ps1 +++ b/windows/tests/start-verified-skin-preserved.tests.ps1 @@ -169,7 +169,7 @@ function Invoke-DreamSkinStartupFixture { # The exact renderer output from #267: theme installed and painted, every # readiness signal true except the native-window probe. $renderedPayload = @' -{"installed":true,"version":"1.5.7","stylePresent":true,"homePresent":true, +{"installed":true,"version":"1.5.8","stylePresent":true,"homePresent":true, "nativeWindow":{"pass":false,"bound":false,"reason":"target-window-unavailable"}, "documentVisibility":"visible","documentHidden":false, "viewport":{"width":1289,"height":829},