Installer: an older feed shows its real reason; a folder with accented letters unpacks (TUI stress #11 A01b/A05, #12 A04)

#11: the refusal of a feed older than the one already accepted now says both sequence numbers, the folder that holds the newer one (.updates) and one next step (use the newest release folder, or delete state.json to take the older one). install.ps1 and install.sh print the whole check output, not its first 6 lines, and no longer blame a missing signature when the check gave a reason.
#12: tar.exe got the path through the ANSI/OEM code page and failed on accented letters. The installer now runs Windows' own bsdtar (System32 first, so a GNU tar from Git for Windows cannot be picked up for a zip) with ASCII relative arguments from inside the data folder and renames the result, so no non-ASCII path ever crosses that boundary. Tested on C:\...\<U-umlaut>n<i-umlaut> test with a stub release folder; the old script fails there with "could not chdir".
This commit is contained in:
Niko1221 committed 2026-10-22 23:54:57 +02:00
1 parent ea67f6999a
commit 0af0f9f125
5 files changed
+132 -12

No files matched your search

+24 -7
View File
@@ -68,12 +68,29 @@ try {
if (Test-Path $incoming) { Remove-Item -Recurse -Force $incoming }
$tooLong = 'If it says a path is too long, set $env:STRATA_DATA_DIR to a short folder such as C:\Strata and run this again; otherwise check the free disk space.'
try {
# tar.exe (Windows 10 1803 and later) handles long paths; Expand-Archive in Windows PowerShell 5.1 does not
$tar = Get-Command tar.exe -ErrorAction SilentlyContinue
# tar.exe (Windows 10 1803 and later) handles long paths; Expand-Archive in Windows PowerShell 5.1 does not.
# tar.exe receives its arguments through the ANSI/OEM code page, so a folder with an accented letter in its name turns into one with a question mark and the
# unpack fails (A04). It is therefore started with ASCII-only, relative arguments from inside the data folder (PowerShell's own cmdlets are Unicode-safe),
# unpacks into a folder with an ASCII name, and that folder is renamed to $incoming.
# Windows' own bsdtar first: a GNU tar from Git for Windows or MSYS earlier on the PATH cannot read a zip
$tar = Get-Command (Join-Path $env:SystemRoot 'System32\tar.exe') -ErrorAction SilentlyContinue
if (-not $tar) { $tar = Get-Command tar.exe -ErrorAction SilentlyContinue }
if ($tar) {
New-Item -ItemType Directory -Force -Path $incoming | Out-Null
$tarOut = & $tar.Source -xf $zip -C $incoming 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { throw $tarOut.Trim() }
$tag = 'u' + [Guid]::NewGuid().ToString('N').Substring(0, 6)
$terminalDir = Join-Path $data 'terminal'
Push-Location -LiteralPath $terminalDir
try {
Copy-Item -LiteralPath $zip -Destination "$tag.zip"
New-Item -ItemType Directory -Path $tag | Out-Null
$tarOut = & $tar.Source -xf "$tag.zip" -C $tag 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { throw $tarOut.Trim() }
}
finally {
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath "$tag.zip"
Pop-Location
}
Move-Item -LiteralPath (Join-Path $terminalDir $tag) -Destination $incoming
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath (Join-Path $terminalDir $tag)
} else { Expand-Archive -LiteralPath $zip -DestinationPath $incoming -Force }
}
catch { Remove-Item -Recurse -Force $incoming -ErrorAction SilentlyContinue; Die "the archive could not be unpacked ($($_.Exception.Message)). $tooLong Nothing was installed." }
@@ -96,9 +113,9 @@ try {
$code = $LASTEXITCODE
$ErrorActionPreference = $prevEap
if ($code -ne 0) {
($out -split "`n" | Select-Object -First 6 | ForEach-Object { " $_" }) | ForEach-Object { [Console]::Error.WriteLine($_) }
($out -split "`r?`n" | Where-Object { $_.Trim() } | ForEach-Object { " $_" }) | ForEach-Object { [Console]::Error.WriteLine($_) }
Remove-Item -Recurse -Force $incoming
Die "the release folder's signed feed could not be verified, so nothing was installed. If you trust this source, ask its owner to publish a signed feed."
Die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
}
Write-Host ' Signature ok.'
+2 -2
View File
@@ -108,9 +108,9 @@ if [ "${STRATA_INSTALL_TEST_TRUST:-}" = 1 ]; then
fi
say "Checking the release's signature…"
if ! "$dest.incoming/bin/strata" update --check --no-engine --feed "$FEED" >"$work/check.txt" 2>&1; then
sed -e 's/^/ /' "$work/check.txt" | head -6 >&2
sed -e 's/^/ /' "$work/check.txt" >&2
rm -rf "$dest.incoming"
die "the release folder's signed feed could not be verified, so nothing was installed. If you trust this source, ask its owner to publish a signed feed."
die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
fi
say " Signature ok."
+7 -2
View File
@@ -51,6 +51,9 @@ class FeedError(RuntimeError):
class FeedDowngrade(FeedError):
"""A feed or a package older than what this PC already has."""
# .sequence is the feed's, .accepted the last one this PC took (the updater adds the folder that holds it)
sequence: int | None = None
accepted: int | None = None
def version_key(value: str) -> tuple[int, ...]:
@@ -314,8 +317,10 @@ def verify_and_parse(payload: bytes, signature_bytes: bytes, *, source: str, tru
if feed.expires_at and _time(feed.expires_at, "expires_at") < now:
raise FeedError(_expired_words(feed.expires_at, now, server_now))
if last_sequence is not None and feed.sequence < last_sequence:
raise FeedDowngrade(f"the update feed is older than the one already accepted (sequence {feed.sequence} < {last_sequence}); "
"refusing a replayed feed")
down = FeedDowngrade(f"the update feed is older than the one already accepted (sequence {feed.sequence} < {last_sequence}); "
"refusing a replayed feed")
down.sequence, down.accepted = feed.sequence, last_sequence
raise down
return feed
+12 -1
View File
@@ -309,11 +309,22 @@ def require_acquisition(what: str) -> None:
acquisition.require(what, "`strata update` from a terminal")
def downgrade_words(sequence, accepted, state_file: Path) -> str:
"""A feed older than the one already accepted (A01b/A05), in plain words: both numbers, the folder that remembers the newer one, and one next step.
The sentence keeps "older than the one already accepted" so a log search for the old text still finds it."""
return (f"this update source is older than the one already accepted on this computer (its sequence is {sequence}, the accepted one is "
f"{accepted}; refusing a replayed feed). The newer one is remembered in {state_file.parent}. Use the newest release folder; "
f"if you really want this older one, delete {state_file} and run this again.")
def open_feed(ctx: Context, state: State) -> feedmod.Feed:
require_acquisition("the update feed")
last = (state.data.get("feed") or {}).get("sequence")
ctx.say("feed", "Reading the signed update feed")
loaded = feedmod.fetch(ctx.source, ctx.trusted, last_sequence=last, channel=ctx.channel, now=ctx.now)
try:
loaded = feedmod.fetch(ctx.source, ctx.trusted, last_sequence=last, channel=ctx.channel, now=ctx.now)
except feedmod.FeedDowngrade as exc:
raise feedmod.FeedDowngrade(downgrade_words(exc.sequence, exc.accepted, state.path)) from None
info = state.section("feed")
info.update({"sequence": loaded.sequence, "key_id": loaded.key_id, "source": ctx.source, "channel": loaded.channel,
"checked_at": (ctx.now or dt.datetime.now(dt.timezone.utc)).isoformat()})
+87
View File
@@ -0,0 +1,87 @@
"""0.2.3 RC stress test: #11 (A01b/A05) an install from an older feed shows the real reason with both sequence numbers, the folder that holds the
old state and one next step, and the installer prints the whole cause; #12 (A04) the Windows installer unpacks into a folder whose name has
accented letters."""
import hashlib
import os
import shutil
import subprocess
import tempfile
import unittest
import zipfile
from pathlib import Path
from strata_control import feed as feedmod, updater
from tests.terminal import test_updater
ROOT = Path(__file__).resolve().parents[2]
INSTALL_PS1 = ROOT / "packaging" / "install.ps1"
POWERSHELL = shutil.which("powershell") if os.name == "nt" else None
class ReplayedFeedWords(test_updater.UpdaterCase):
def test_the_refusal_gives_both_numbers_the_folder_and_one_next_step(self):
self.publish(("0.1.46", test_updater.fx.GOOD_ENGINE), sequence=108)
updater.check(self.ctx()) # accepts sequence 108 and remembers it
self.publish(("0.1.46", test_updater.fx.GOOD_ENGINE), sequence=107)
with self.assertRaises(feedmod.FeedDowngrade) as caught:
updater.check(self.ctx())
text = str(caught.exception)
self.assertIn("older than the one already accepted", text)
self.assertIn("its sequence is 107", text)
self.assertIn("the accepted one is 108", text)
self.assertIn(str(self.components / ".updates"), text)
self.assertIn("state.json", text)
self.assertEqual(text.count("Use the newest release folder"), 1)
def stub_release(folder: Path, body: str) -> str:
folder.mkdir(parents=True)
archive = folder / "terminal.zip"
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("strata-terminal-0.1.48-windows-x86_64/bin/strata.cmd", body)
z.writestr("strata-terminal-0.1.48-windows-x86_64/strata.py", "# stub\n")
(folder / "install-manifest.txt").write_text(
f"windows-x86_64 0.1.48 terminal.zip {hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.stat().st_size}\n")
return "file:///" + folder.as_posix()
@unittest.skipUnless(POWERSHELL, "the Windows installer runs only on Windows")
class WindowsInstaller(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="strata-inst-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def run_installer(self, feed: str, data: Path):
runner = self.root / "run.ps1"
runner.write_text("$ErrorActionPreference = 'Continue'\n"
f"$env:STRATA_FEED = '{feed}'; $env:STRATA_DATA_DIR = '{data}'; $env:STRATA_NO_PATH = '1'\n"
f"try {{ Invoke-Expression (Get-Content -Raw -LiteralPath '{INSTALL_PS1}') }} catch {{ Write-Host \"STOPPED\" }}\n",
encoding="utf-8-sig")
done = subprocess.run([POWERSHELL, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(runner)], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=300)
return done.stdout + done.stderr
def test_a_folder_with_accented_letters_is_unpacked(self):
feed = stub_release(self.root / "feed", "@echo off\r\nexit /b 0\r\n")
data = self.root / "Ünï test"
out = self.run_installer(feed, data)
self.assertIn("Installed. Run: strata launch", out, out)
self.assertTrue((data / "terminal" / "0.1.48" / "bin" / "strata.cmd").is_file())
self.assertEqual([p.name for p in (data / "terminal").iterdir() if p.name.startswith("u") and p.suffix in ("", ".zip")], [])
def test_the_whole_cause_is_printed_not_the_first_six_lines(self):
lines = ["echo strata update: this update source is older than the one already accepted (its sequence is 107, the accepted one is 108)."]
lines += [f"echo detail {i}" for i in range(2, 9)] + ["echo the last line of the cause"]
feed = stub_release(self.root / "feed2", "@echo off\r\n" + "\r\n".join(lines) + "\r\nexit /b 5\r\n")
out = self.run_installer(feed, self.root / "data")
self.assertIn("the last line of the cause", out)
self.assertIn("its sequence is 107", out)
self.assertIn("The reason is printed above", out)
self.assertIn("STOPPED", out)
self.assertFalse((self.root / "data" / "terminal" / "0.1.48").exists(), "nothing is installed")
if __name__ == "__main__":
unittest.main()