From bd1e2885dfef22a3fd97b9776c4ec8765823f1bf Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 10 Jun 2026 08:56:32 +0100 Subject: [PATCH] feat(toolbox): preconfig store (load fail-open, clear-pin, configured marker) Co-Authored-By: Claude Sonnet 4.6 --- .../Preconfig/IPreconfigStore.cs | 9 +++ .../Preconfig/Preconfig.cs | 22 +++++++ .../Preconfig/PreconfigStore.cs | 30 +++++++++ .../SilverOS.Welcome.Tests/PreconfigTests.cs | 64 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 windows/welcome/src/SilverOS.Welcome.Core/Preconfig/IPreconfigStore.cs create mode 100644 windows/welcome/src/SilverOS.Welcome.Core/Preconfig/Preconfig.cs create mode 100644 windows/welcome/src/SilverOS.Welcome.Core/Preconfig/PreconfigStore.cs create mode 100644 windows/welcome/tests/SilverOS.Welcome.Tests/PreconfigTests.cs diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/IPreconfigStore.cs b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/IPreconfigStore.cs new file mode 100644 index 0000000..c74fa40 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/IPreconfigStore.cs @@ -0,0 +1,9 @@ +namespace SilverOS.Welcome.Core.Preconfig; + +public interface IPreconfigStore +{ + Preconfig? Load(); // null if missing/corrupt (fail-open) + void ClearPin(); // rewrite preconfig without the BitLocker pin + bool IsConfigured(); // configured marker present? + void MarkConfigured(); // write the configured marker +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/Preconfig.cs b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/Preconfig.cs new file mode 100644 index 0000000..2164d00 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/Preconfig.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace SilverOS.Welcome.Core.Preconfig; + +public sealed record BitlockerConfig { public bool Enable { get; init; } public string? Pin { get; init; } } +public sealed record AppsConfig { public bool UseFlavourDefaults { get; init; } = true; public IReadOnlyList? Selected { get; init; } } + +public sealed record Preconfig +{ + public int SchemaVersion { get; init; } = 1; + public string Flavour { get; init; } = ""; + public BitlockerConfig Bitlocker { get; init; } = new(); + public AppsConfig Apps { get; init; } = new(); + + public static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/PreconfigStore.cs b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/PreconfigStore.cs new file mode 100644 index 0000000..43771da --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Preconfig/PreconfigStore.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +namespace SilverOS.Welcome.Core.Preconfig; + +public sealed class PreconfigStore(string dir) : IPreconfigStore +{ + private string File_ => Path.Combine(dir, "preconfig.json"); + private string Marker => Path.Combine(dir, "configured"); + + public Preconfig? Load() + { + try + { + if (!File.Exists(File_)) return null; + return JsonSerializer.Deserialize(File.ReadAllText(File_), Preconfig.JsonOptions); + } + catch (JsonException) { return null; } // fail-open + } + + public void ClearPin() + { + var p = Load(); + if (p is null) return; + var stripped = p with { Bitlocker = p.Bitlocker with { Pin = null } }; + File.WriteAllText(File_, JsonSerializer.Serialize(stripped, Preconfig.JsonOptions)); + } + + public bool IsConfigured() => File.Exists(Marker); + public void MarkConfigured() { Directory.CreateDirectory(dir); File.WriteAllText(Marker, "1"); } +} diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/PreconfigTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/PreconfigTests.cs new file mode 100644 index 0000000..2f63df7 --- /dev/null +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/PreconfigTests.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Text.Json; +using SilverOS.Welcome.Core.Preconfig; +using Xunit; + +public class PreconfigTests +{ + static string TempDir() + { + var d = Path.Combine(Path.GetTempPath(), "smpre-" + System.Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(d); + return d; + } + + const string Sample = """ + { "schemaVersion":1, "flavour":"developer", + "bitlocker":{"enable":true,"pin":"246810"}, + "apps":{"useFlavourDefaults":true} } + """; + + [Fact] + public void Loads_flavour_and_pin() + { + var dir = TempDir(); + File.WriteAllText(Path.Combine(dir, "preconfig.json"), Sample); + var p = new PreconfigStore(dir).Load(); + Assert.NotNull(p); + Assert.Equal("developer", p!.Flavour); + Assert.True(p.Bitlocker.Enable); + Assert.Equal("246810", p.Bitlocker.Pin); + Assert.True(p.Apps.UseFlavourDefaults); + } + + [Fact] + public void Missing_or_bad_file_returns_null_not_throw() + { + Assert.Null(new PreconfigStore(TempDir()).Load()); // missing + var dir = TempDir(); + File.WriteAllText(Path.Combine(dir, "preconfig.json"), "{ not json"); + Assert.Null(new PreconfigStore(dir).Load()); // corrupt + } + + [Fact] + public void ClearPin_rewrites_without_the_pin() + { + var dir = TempDir(); + File.WriteAllText(Path.Combine(dir, "preconfig.json"), Sample); + var store = new PreconfigStore(dir); + store.ClearPin(); + var reread = store.Load(); + Assert.True(string.IsNullOrEmpty(reread!.Bitlocker.Pin)); + Assert.Equal("developer", reread.Flavour); // rest preserved + } + + [Fact] + public void Configured_marker_roundtrips() + { + var dir = TempDir(); + var store = new PreconfigStore(dir); + Assert.False(store.IsConfigured()); + store.MarkConfigured(); + Assert.True(store.IsConfigured()); + } +}