Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Store/Configuration/Config.Access.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public void SetOption(string key, string value)
throw new UnauthorizedAccessException(Resources.OptionLockedByPolicy);

_metaData[key].Value = value;
_explicitlySetOptions.Add(key);
}

/// <summary>
Expand All @@ -40,7 +41,10 @@ public void SetOption(string key, string value)
/// <exception cref="KeyNotFoundException"><paramref name="key"/> is invalid.</exception>
[RequiresUnreferencedCode("Relies on [DefaultValue], which is not trim-safe.")]
public void ResetOption(string key)
=> SetOption(key, _metaData[key].DefaultValue);
{
_metaData[key].Value = _metaData[key].DefaultValue;
_explicitlySetOptions.Remove(key);
}

/// <summary>
/// Creates a deep copy of this <see cref="Config"/> instance.
Expand Down
12 changes: 11 additions & 1 deletion src/Store/Configuration/Config.Storage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ public void ReadFromFilesMachineWideOnly()
[NonSerialized]
private IniData? _lastIniFromFile;

/// <summary>
/// Tracks which options have been explicitly set in the config file.
/// This allows overriding machine-wide config options back to their default values.
/// </summary>
[NonSerialized]
private readonly HashSet<string> _explicitlySetOptions = [];

/// <summary>
/// Reads options from a config file and merges them into the config instance.
/// </summary>
Expand Down Expand Up @@ -142,6 +149,7 @@ public void ReadFrom(IniData iniData, string path = "embedded")
property.Value = property.NeedsEncoding
? global[effectiveKey].Base64Utf8Decode()
: global[effectiveKey];
_explicitlySetOptions.Add(key);
}
#region Error handling
catch (FormatException ex)
Expand Down Expand Up @@ -276,7 +284,9 @@ public IniData ToIniData()
? key + Base64Suffix
: key;

if (property.IsDefaultValue)
// Save the property if it's not default OR if it was explicitly set in the config file
// This allows overriding machine-wide config options back to their default values
if (property.IsDefaultValue && !_explicitlySetOptions.Contains(key))
global.RemoveKey(effectiveKey);
else
{
Expand Down
108 changes: 108 additions & 0 deletions src/UnitTests/Store/Configuration/ConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,112 @@ public void LoadStressTest()

StressTest.Run(() => _ = Config.Load());
}

/// <summary>
/// Ensures that user config can override machine-wide config back to default values.
/// </summary>
[Fact]
public void OverrideMachineWideWithDefault()
{
using var machineWideFile = new TemporaryFile("0install-test-machine-config");
using var userFile = new TemporaryFile("0install-test-user-config");

// Machine-wide config sets a non-default value
var machineWideConfig = new Config { NetworkUse = NetworkLevel.Minimal };
machineWideConfig.Save(machineWideFile);

// User wants to override back to default (Full)
var userConfig = new Config();
userConfig.ReadFromFile(machineWideFile); // Load machine-wide first
userConfig.SetOption("network_use", "full"); // Explicitly override to default
userConfig.Save(userFile);

// Load both configs (machine-wide first, then user)
var loadedConfig = new Config();
loadedConfig.ReadFromFile(machineWideFile);
loadedConfig.ReadFromFile(userFile);

// User's override should be respected
loadedConfig.NetworkUse.Should().Be(NetworkLevel.Full,
because: "User config should be able to override machine-wide config back to default value");
}

/// <summary>
/// Ensures that default values are not saved when no override is needed.
/// </summary>
[Fact]
public void DefaultValuesNotSavedWithoutOverride()
{
using var tempFile = new TemporaryFile("0install-test-config");

// Create config with all default values
var config = new Config();
config.Save(tempFile);

// The file should be minimal (only contain the section header)
string contents = File.ReadAllText(tempFile);
contents.Should().NotContain("network_use",
because: "Default values should not be saved when not explicitly set");
contents.Should().NotContain("help_with_testing",
because: "Default values should not be saved when not explicitly set");
}

/// <summary>
/// Ensures that explicitly set default values persist across save/load cycles.
/// </summary>
[Fact]
public void ExplicitDefaultValuesPersist()
{
using var tempFile = new TemporaryFile("0install-test-config");

// Explicitly set a value to its default using SetOption
var config1 = new Config();
config1.SetOption("help_with_testing", "false"); // Explicitly set to default
config1.Save(tempFile);

// The explicitly set default should be in the file
string contents = File.ReadAllText(tempFile);
contents.Should().Contain("help_with_testing",
because: "Explicitly set default values should be saved");

// Load it back
var config2 = new Config();
config2.ReadFromFile(tempFile);

// Save again
config2.Save(tempFile);

// The value should still be in the file
string contents2 = File.ReadAllText(tempFile);
contents2.Should().Contain("help_with_testing",
because: "Explicitly set default values should persist across save/load cycles");
}

/// <summary>
/// Ensures that ResetOption removes the explicit override, allowing inheritance from machine-wide config.
/// </summary>
[Fact]
public void ResetOptionRemovesOverride()
{
using var tempFile = new TemporaryFile("0install-test-config");

// Explicitly set a value
var config1 = new Config();
config1.SetOption("help_with_testing", "true");
config1.Save(tempFile);

// Verify it's saved
File.ReadAllText(tempFile).Should().Contain("help_with_testing");

// Load it back and reset
var config2 = new Config();
config2.ReadFromFile(tempFile);
config2.ResetOption("help_with_testing");
config2.Save(tempFile);

// The reset should remove it from the file (since it's now at default)
string contents = File.ReadAllText(tempFile);
contents.Should().NotContain("help_with_testing",
because: "ResetOption should remove the explicit override");
}
}