fix(defaults): treat a read-denied defaults file as invalid

NestDefaults.Load caught only JsonException and IOException, so a
defaults.json that File.Exists can see but the process cannot read
threw UnauthorizedAccessException out of New. Report such files as
Invalid and return the fallback values, like a corrupt file.

The regression mode-000s the file on Unix (skipped on Windows and when
running as root); it failed with UnauthorizedAccessException before
this change.
This commit is contained in:
aj committed 2026-10-04 11:59:08 -04:00
1 parent 23d258b886
commit 39a15aee14
2 files changed
+42 -6

No files matched your search

+11 -6
View File
@@ -88,13 +88,10 @@ public sealed class NestDefaults
var json = File.ReadAllText(path);
dto = JsonSerializer.Deserialize<NestDefaultsDto>(json, JsonOptions);
}
catch (JsonException)
{
status = NestDefaultsStatus.Invalid;
return defaults;
}
catch (IOException)
catch (Exception ex) when (IsUnreadableFile(ex))
{
// A file that exists but cannot be read (locked, access-denied)
// or parsed must never block creating a nest.
status = NestDefaultsStatus.Invalid;
return defaults;
}
@@ -228,6 +225,14 @@ public sealed class NestDefaults
}
}
private static bool IsUnreadableFile(Exception ex) =>
ex
is JsonException
or IOException
or UnauthorizedAccessException
or NotSupportedException
or System.Security.SecurityException;
private static bool IsValidSize(double width, double length) =>
!double.IsNaN(width)
&& !double.IsNaN(length)
+31
View File
@@ -75,6 +75,24 @@ public class NestDefaultsTests : IDisposable
AssertFallback(loaded);
}
[SkippableFact]
public void Load_ReadDeniedFile_ReturnsFallbackButReportsInvalid()
{
if (OperatingSystem.IsWindows())
throw new SkipException("Unix file modes deny the read on Linux/macOS");
new NestDefaults { PartSpacing = 4 }.Save(_path);
File.SetUnixFileMode(_path, UnixFileMode.None);
Skip.If(CanRead(_path), "Process can read a mode-000 file (running as root)");
// The file is visible to File.Exists, but reading it is denied.
Assert.True(File.Exists(_path));
var loaded = NestDefaults.Load(_path, out var status);
Assert.Equal(NestDefaultsStatus.Invalid, status);
AssertFallback(loaded);
}
[Fact]
public void Load_PartialFile_MergesPerField()
{
@@ -198,6 +216,19 @@ public class NestDefaultsTests : IDisposable
Assert.Equal(new Size(48, 96), defaults.Size);
}
private static bool CanRead(string path)
{
try
{
using var _ = File.OpenRead(path);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
}
private static void AssertFallback(NestDefaults loaded)
{
Assert.Equal(Units.Inches, loaded.Units);