From e5ff0c2d033cea8ad4a8edacac300168aa4f4346 Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Tue, 6 Oct 2026 13:36:09 +0200 Subject: [PATCH 1/2] fix: treat every read error in a storage file as a corrupted file --- src/Runtime/Persistence/StorageFormat.cs | 14 ++++++++++++-- src/Runtime/Persistence/StorageSerializer.cs | 8 +++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Runtime/Persistence/StorageFormat.cs b/src/Runtime/Persistence/StorageFormat.cs index f191311..b6dd213 100644 --- a/src/Runtime/Persistence/StorageFormat.cs +++ b/src/Runtime/Persistence/StorageFormat.cs @@ -65,8 +65,18 @@ internal static void Write(BinaryWriter writer, IReadOnlyList sec internal static void ReadFile(string storageFilePath, IReadOnlyList sections, Dictionary data, KeyLoadFailedBehaviour keyLoadFailedBehaviour) { - using var stream = new FileStream(storageFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ReadBufferSize); - using var reader = new BinaryReader(stream, Encoding.UTF8); + using var stream = OpenFile(storageFilePath); + Read(storageFilePath, stream, sections, data, keyLoadFailedBehaviour); + } + + internal static FileStream OpenFile(string storageFilePath) + { + return new FileStream(storageFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ReadBufferSize); + } + + internal static void Read(string storageFilePath, FileStream stream, IReadOnlyList sections, Dictionary data, KeyLoadFailedBehaviour keyLoadFailedBehaviour) + { + using var reader = new BinaryReader(stream, Encoding.UTF8, true); var fileSections = ReadHeader(storageFilePath, sections, reader, out var recordCount); var fileLength = stream.Length; diff --git a/src/Runtime/Persistence/StorageSerializer.cs b/src/Runtime/Persistence/StorageSerializer.cs index e3f92ca..881c2fe 100644 --- a/src/Runtime/Persistence/StorageSerializer.cs +++ b/src/Runtime/Persistence/StorageSerializer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -41,16 +42,17 @@ public StorageSnapshot Serialize(Dictionary data) public bool TryDeserialize(string filePath, Dictionary data, KeyLoadFailedBehaviour keyLoadFailedBehaviour, out StorageFileCorruptedException failure) { + using var stream = StorageFormat.OpenFile(filePath); try { - StorageFormat.ReadFile(filePath, _sections, data, keyLoadFailedBehaviour); + StorageFormat.Read(filePath, stream, _sections, data, keyLoadFailedBehaviour); failure = null; return true; } - catch (StorageFileCorruptedException exception) + catch (Exception exception) when (exception is not KeyLoadFailedException) { Clear(data); - failure = exception; + failure = exception as StorageFileCorruptedException ?? new StorageFileCorruptedException(filePath, exception.Message, exception); return false; } } From 3dc6257f909df68025f9afb9d9aa45d46dea2dd0 Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Tue, 6 Oct 2026 13:36:10 +0200 Subject: [PATCH 2/2] feat: add a builder setting for a storage file that cannot be recovered --- CHANGELOG.md | 2 + README.md | 8 + src/Runtime/BinaryStorage.Builder.cs | 15 +- src/Runtime/BinaryStorage.cs | 6 +- src/Runtime/Persistence/StoragePersistence.cs | 22 +- .../Settings/CorruptedFileBehaviour.cs | 23 ++ .../Settings/CorruptedFileBehaviour.cs.meta | 2 + src/Tests/BaseStorageTests.cs | 5 +- src/Tests/Persistence/BackupRecoveryTests.cs | 8 +- .../CorruptedFileBehaviourTests.cs | 234 ++++++++++++++++++ .../CorruptedFileBehaviourTests.cs.meta | 2 + src/Tests/Persistence/CorruptedLoadTests.cs | 6 +- src/Tests/Persistence/TruncatedEntryTests.cs | 2 +- 13 files changed, 318 insertions(+), 17 deletions(-) create mode 100644 src/Runtime/Settings/CorruptedFileBehaviour.cs create mode 100644 src/Runtime/Settings/CorruptedFileBehaviour.cs.meta create mode 100644 src/Tests/Persistence/CorruptedFileBehaviourTests.cs create mode 100644 src/Tests/Persistence/CorruptedFileBehaviourTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index ffcbca5..4f8a360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed - Storage files are now written on a shared background thread. Changes are still serialized on the calling thread, but `Set` and auto-save no longer wait for the disk. `Save()` and `Dispose()` still block until the data has reached the disk. +- `Build()` no longer throws when neither the storage file nor its backup can be read: the storage starts empty and logs the `StorageFileCorruptedException`. Every read error in the file now counts as a corrupted file, while an error on opening it still reaches the caller. ### Added - `SaveOnBackgroundThread(bool)` on the builder. Pass `false` to write the file before every change returns, as before. +- `SetCorruptedFileBehaviour(CorruptedFileBehaviour)` on the builder. Pass `ThrowException` to get the exception from `Build()`, or `ResetToEmpty` to start empty without a log. ### Removed - `IsDirty`. With auto-save on it was almost always `false`, and with the background writer it meant "no change is waiting to be handed over", not "everything is on disk". Use `Save()` when you need the data on disk. diff --git a/README.md b/README.md index a49d9c2..7311403 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ using var storage = BinaryStorage.Construct(path) .SupportDictionariesOf() // dictionary of supported types .SetMissingKeyBehaviour(MissingKeyBehavior.ReturnDefaultValueOnly) .SetTypeMismatchBehaviour(TypeMismatchBehaviour.OverrideValueAndType) + .SetCorruptedFileBehaviour(CorruptedFileBehaviour.ResetToEmptyWithError) .EnableAutoSaveOnChange() .SaveOnBackgroundThread(true) // on by default, false writes the file before every change returns .Build(KeyLoadFailedBehaviour.IgnoreWithWarning); @@ -227,6 +228,13 @@ Passed to `Build`; controls what happens when a key fails to deserialize on load - `Ignore` - skip the bad key silently. - `IgnoreWithWarning` - skip the bad key and log a warning (default). + +### CorruptedFileBehaviour +Controls what `Build` does when neither the storage file nor its backup can be read. In every mode the unreadable files are removed, so the next start is clean. An error on opening the file (no access, the file is locked) is not a corrupted file: it always reaches the caller and the files stay. +- `ThrowException` - throw a `StorageFileCorruptedException`. +- `ResetToEmpty` - start with an empty storage. +- `ResetToEmptyWithError` - start with an empty storage and log the exception (default). + Per-call overrides are available too: `Get(key, default, overrideMissingKeyBehavior)` and `Set(key, value, overrideTypeMismatchBehaviour)`. ## License diff --git a/src/Runtime/BinaryStorage.Builder.cs b/src/Runtime/BinaryStorage.Builder.cs index e477e5b..be87099 100644 --- a/src/Runtime/BinaryStorage.Builder.cs +++ b/src/Runtime/BinaryStorage.Builder.cs @@ -50,6 +50,7 @@ public class Builder private bool _saveJsonForDebug; private MissingKeyBehavior _missingKeyBehavior = MissingKeyBehavior.InitializeWithDefaultValue; private TypeMismatchBehaviour _typeMismatchBehaviour = TypeMismatchBehaviour.ThrowException; + private CorruptedFileBehaviour _corruptedFileBehaviour = CorruptedFileBehaviour.ResetToEmptyWithError; internal Builder(string filePath) { @@ -105,6 +106,16 @@ public Builder SetTypeMismatchBehaviour(TypeMismatchBehaviour behavior) return this; } + /// Specifies the behavior when neither the storage file nor its backup can be read on load. + /// In every mode the unreadable files are removed, so the next load starts clean. An error while the file is opened is not a corrupted file: it always reaches the caller and leaves the files untouched. + /// The corrupted file behavior. + /// The current instance for method chaining. + public Builder SetCorruptedFileBehaviour(CorruptedFileBehaviour behavior) + { + _corruptedFileBehaviour = behavior; + return this; + } + /// Adds serializers for primitive types to the storage configuration. /// The current instance for method chaining. public Builder AddPrimitiveTypes() @@ -248,7 +259,7 @@ public Builder SupportDictionariesOf() /// The configured instance. /// Thrown if the storage is disposed. /// An I/O error occurred - /// The storage file structure is corrupted (bad header, truncated framing, or a duplicate key). + /// Neither the storage file nor its backup can be read and the corrupted file behavior is . /// A key failed to load and is . public BinaryStorage Build(KeyLoadFailedBehaviour keyLoadFailedBehaviour = KeyLoadFailedBehaviour.IgnoreWithWarning) { @@ -257,7 +268,7 @@ public BinaryStorage Build(KeyLoadFailedBehaviour keyLoadFailedBehaviour = KeyLo storage.SaveJsonCopyForDebug = _saveJsonForDebug; storage.MissingKeyBehavior = _missingKeyBehavior; storage.TypeMismatchBehaviour = _typeMismatchBehaviour; - storage.LoadDataFromDisk(keyLoadFailedBehaviour); + storage.LoadDataFromDisk(keyLoadFailedBehaviour, _corruptedFileBehaviour); LockFilePathInEditor(_filePath); return storage; } diff --git a/src/Runtime/BinaryStorage.cs b/src/Runtime/BinaryStorage.cs index d57427f..268d27d 100644 --- a/src/Runtime/BinaryStorage.cs +++ b/src/Runtime/BinaryStorage.cs @@ -698,12 +698,12 @@ public virtual void Dispose() /// Loads the data from disk into memory. /// Thrown if the storage is disposed. /// An I/O error occurred - /// The file structure is corrupted (bad header, truncated framing, or a duplicate key). + /// Neither the file nor its backup can be read and is . /// A key failed to load and is . - private void LoadDataFromDisk(KeyLoadFailedBehaviour keyLoadFailedBehaviour) + private void LoadDataFromDisk(KeyLoadFailedBehaviour keyLoadFailedBehaviour, CorruptedFileBehaviour corruptedFileBehaviour) { ThrowIfDisposed(); - _persistence.Load(_data, keyLoadFailedBehaviour); + _persistence.Load(_data, keyLoadFailedBehaviour, corruptedFileBehaviour); foreach (var pair in _data) { TrackCollectionOf(pair.Value, pair.Key); diff --git a/src/Runtime/Persistence/StoragePersistence.cs b/src/Runtime/Persistence/StoragePersistence.cs index cce9df3..b4b06fe 100644 --- a/src/Runtime/Persistence/StoragePersistence.cs +++ b/src/Runtime/Persistence/StoragePersistence.cs @@ -20,10 +20,28 @@ public StoragePersistence(string filePath, IReadOnlyList sections _writer = saveOnBackgroundThread ? new BackgroundStorageWriter(_file) : new ImmediateStorageWriter(_file); } - public void Load(Dictionary data, KeyLoadFailedBehaviour keyLoadFailedBehaviour) + public void Load(Dictionary data, KeyLoadFailedBehaviour keyLoadFailedBehaviour, CorruptedFileBehaviour corruptedFileBehaviour) { _serializer.Clear(data); - _file.Load((string filePath, out StorageFileCorruptedException failure) => _serializer.TryDeserialize(filePath, data, keyLoadFailedBehaviour, out failure)); + try + { + _file.Load((string filePath, out StorageFileCorruptedException failure) => _serializer.TryDeserialize(filePath, data, keyLoadFailedBehaviour, out failure)); + } + catch (StorageFileCorruptedException exception) + { + switch (corruptedFileBehaviour) + { + case CorruptedFileBehaviour.ThrowException: + throw; + case CorruptedFileBehaviour.ResetToEmpty: + break; + case CorruptedFileBehaviour.ResetToEmptyWithError: + Debug.LogException(exception); + break; + default: + throw new UnexpectedEnumException(typeof(CorruptedFileBehaviour), corruptedFileBehaviour); + } + } } public void Save(Dictionary data, bool waitForDisk) diff --git a/src/Runtime/Settings/CorruptedFileBehaviour.cs b/src/Runtime/Settings/CorruptedFileBehaviour.cs new file mode 100644 index 0000000..4f71d72 --- /dev/null +++ b/src/Runtime/Settings/CorruptedFileBehaviour.cs @@ -0,0 +1,23 @@ +namespace Appegy.Storage +{ + /// + /// Specifies the behavior when neither the storage file nor its backup can be read on load. + /// + public enum CorruptedFileBehaviour + { + /// + /// Throws a from Build. + /// + ThrowException, + + /// + /// Starts with an empty storage. + /// + ResetToEmpty, + + /// + /// Starts with an empty storage and logs the exception. + /// + ResetToEmptyWithError, + } +} diff --git a/src/Runtime/Settings/CorruptedFileBehaviour.cs.meta b/src/Runtime/Settings/CorruptedFileBehaviour.cs.meta new file mode 100644 index 0000000..83ad59a --- /dev/null +++ b/src/Runtime/Settings/CorruptedFileBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 842cb68f71ae407f9dc71102939d2ad5 \ No newline at end of file diff --git a/src/Tests/BaseStorageTests.cs b/src/Tests/BaseStorageTests.cs index 2ef29d3..c49d8af 100644 --- a/src/Tests/BaseStorageTests.cs +++ b/src/Tests/BaseStorageTests.cs @@ -19,11 +19,12 @@ public void CleanStorageBetweenTests() BinaryStorage.Delete(StoragePath); } - protected BinaryStorage Open(bool autoSave = false, bool saveOnBackgroundThread = true) + protected BinaryStorage Open(bool autoSave = false, bool saveOnBackgroundThread = true, CorruptedFileBehaviour corruptedFileBehaviour = CorruptedFileBehaviour.ResetToEmptyWithError) { var builder = BinaryStorage.Construct(StoragePath) .AddPrimitiveTypes() - .SaveOnBackgroundThread(saveOnBackgroundThread); + .SaveOnBackgroundThread(saveOnBackgroundThread) + .SetCorruptedFileBehaviour(corruptedFileBehaviour); if (autoSave) { builder = builder.EnableAutoSaveOnChange(); diff --git a/src/Tests/Persistence/BackupRecoveryTests.cs b/src/Tests/Persistence/BackupRecoveryTests.cs index 4e3825b..30c2210 100644 --- a/src/Tests/Persistence/BackupRecoveryTests.cs +++ b/src/Tests/Persistence/BackupRecoveryTests.cs @@ -168,7 +168,7 @@ public void WhenStorageIsCorruptedAndBackupIsCut_ThenThrowsAndRemovesBoth() var generations = WriteGenerations(2); PutFiles(Array.Empty(), null, Cut(generations[0])); - FluentActions.Invoking(() => Open().Dispose()).Should().Throw(); + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); File.Exists(StoragePath).Should().BeFalse(); File.Exists(BackupPath).Should().BeFalse(); @@ -179,7 +179,7 @@ public void WhenStorageAndBackupAreBothCorrupted_ThenTempIsKept() { PutFiles(Array.Empty(), new byte[] { 1, 2, 3 }, Array.Empty()); - FluentActions.Invoking(() => Open().Dispose()).Should().Throw(); + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); File.Exists(TempPath).Should().BeTrue(); } @@ -191,7 +191,7 @@ public void WhenStorageAndBackupAreBothCorrupted_ThenThrowsAndRemovesBoth() File.WriteAllBytes(StoragePath, Array.Empty()); File.WriteAllBytes(BackupPath, Array.Empty()); - FluentActions.Invoking(() => Open().Dispose()).Should().Throw(); + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); File.Exists(StoragePath).Should().BeFalse(); File.Exists(BackupPath).Should().BeFalse(); @@ -203,7 +203,7 @@ public void WhenRestartedAfterTotalFailure_ThenStartsCleanWithoutThrowing() WriteTwoGenerations(); File.WriteAllBytes(StoragePath, Array.Empty()); File.WriteAllBytes(BackupPath, Array.Empty()); - FluentActions.Invoking(() => Open().Dispose()).Should().Throw(); + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); using var storage = Open(); diff --git a/src/Tests/Persistence/CorruptedFileBehaviourTests.cs b/src/Tests/Persistence/CorruptedFileBehaviourTests.cs new file mode 100644 index 0000000..fd615d9 --- /dev/null +++ b/src/Tests/Persistence/CorruptedFileBehaviourTests.cs @@ -0,0 +1,234 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using Appegy.Storage.Serializers; +using FluentAssertions; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Appegy.Storage +{ + public class CorruptedFileBehaviourTests : BaseStorageTests + { + private static readonly byte[] NegativeStringLength = { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F }; + + private static readonly string[] Damages = + { + "EmptyFile", + "CutHeader", + "CutRecord", + "GarbageTypeNameLength", + "GarbageKeyLength", + "NegativeRecordSize", + "TooLargeRecordSize", + }; + + private static readonly CorruptedFileBehaviour[] Behaviours = + { + CorruptedFileBehaviour.ThrowException, + CorruptedFileBehaviour.ResetToEmpty, + CorruptedFileBehaviour.ResetToEmptyWithError, + }; + + [TestCaseSource(nameof(Damages))] + public void WhenStorageIsDamaged_ThenDataIsRecoveredFromBackup(string damage) + { + var healthy = Healthy(); + File.WriteAllBytes(BackupPath, healthy); + File.WriteAllBytes(StoragePath, Damaged(damage, healthy)); + + using var storage = Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException); + + storage.Get("generation").Should().Be(1); + } + + [TestCaseSource(nameof(Damages))] + public void WhenStorageIsDamagedWithoutBackup_AndThrow_ThenThrowsCorruptedAndRemovesFile(string damage) + { + File.WriteAllBytes(StoragePath, Damaged(damage, Healthy())); + + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); + + File.Exists(StoragePath).Should().BeFalse(); + } + + [Test] + public void WhenStorageAndBackupAreDamaged_AndThrow_ThenThrowsAndLeavesNoDamagedFile() + { + PutDamagedStorageAndBackup(); + + FluentActions.Invoking(() => Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException).Dispose()).Should().Throw(); + + ShouldLeaveNoDamagedFile(); + } + + [Test] + public void WhenStorageAndBackupAreDamaged_AndResetToEmpty_ThenBuildsEmptyAndLeavesNoDamagedFile() + { + PutDamagedStorageAndBackup(); + + using (var storage = Open(corruptedFileBehaviour: CorruptedFileBehaviour.ResetToEmpty)) + { + storage.Keys.Count.Should().Be(0); + } + + LogAssert.NoUnexpectedReceived(); + ShouldLeaveNoDamagedFile(); + } + + [Test] + public void WhenStorageAndBackupAreDamaged_AndResetToEmptyWithError_ThenBuildsEmptyLogsOnceAndLeavesNoDamagedFile() + { + PutDamagedStorageAndBackup(); + + LogAssert.Expect(LogType.Exception, new Regex("invalid string length")); + using (var storage = Open(corruptedFileBehaviour: CorruptedFileBehaviour.ResetToEmptyWithError)) + { + storage.Keys.Count.Should().Be(0); + } + + LogAssert.NoUnexpectedReceived(); + ShouldLeaveNoDamagedFile(); + } + + [Test] + public void WhenBehaviourIsNotSet_ThenDefaultsToResetToEmptyWithError() + { + PutDamagedStorageAndBackup(); + + LogAssert.Expect(LogType.Exception, new Regex("invalid string length")); + using (var storage = BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build()) + { + storage.Keys.Count.Should().Be(0); + } + + LogAssert.NoUnexpectedReceived(); + ShouldLeaveNoDamagedFile(); + } + + [Test] + public void WhenStorageCannotBeOpened_ThenErrorReachesCallerAndFileIsKept([ValueSource(nameof(Behaviours))] CorruptedFileBehaviour behaviour) + { + File.WriteAllBytes(StoragePath, Healthy()); + + using (new FileStream(StoragePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + FluentActions.Invoking(() => Open(corruptedFileBehaviour: behaviour).Dispose()).Should().Throw(); + } + + using var storage = Open(corruptedFileBehaviour: CorruptedFileBehaviour.ThrowException); + storage.Get("generation").Should().Be(1); + } + + [Test] + public void WhenKeyFailsToLoad_AndKeyLoadFailedThrows_ThenFileIsKept() + { + var bytes = WithUnknownType(Healthy()); + File.WriteAllBytes(StoragePath, bytes); + + FluentActions.Invoking(() => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build(KeyLoadFailedBehaviour.ThrowException)).Should().Throw(); + + File.ReadAllBytes(StoragePath).Should().Equal(bytes); + } + + private byte[] Healthy() + { + using (var storage = Open()) + { + storage.Set("generation", 1); + storage.Save(); + } + var bytes = File.ReadAllBytes(StoragePath); + BinaryStorage.Delete(StoragePath); + return bytes; + } + + private void PutDamagedStorageAndBackup() + { + File.WriteAllBytes(StoragePath, Damaged("GarbageKeyLength", null)); + File.WriteAllBytes(BackupPath, Damaged("GarbageTypeNameLength", null)); + } + + private void ShouldLeaveNoDamagedFile() + { + File.Exists(StoragePath).Should().BeFalse(); + File.Exists(BackupPath).Should().BeFalse(); + } + + private static byte[] Damaged(string damage, byte[] healthy) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream, Encoding.UTF8); + switch (damage) + { + case "EmptyFile": + break; + case "CutHeader": + writer.Write(healthy[..3]); + break; + case "CutRecord": + writer.Write(healthy[..^2]); + break; + case "GarbageTypeNameLength": + writer.Write(PackageInfo.Version); + writer.Write(0L); + writer.Write(1); + writer.Write(NegativeStringLength); + break; + case "GarbageKeyLength": + WriteHeader(writer); + writer.Write(NegativeStringLength); + break; + case "NegativeRecordSize": + WriteHeader(writer); + writer.Write("generation"); + writer.Write(0); + writer.Write(-8L); + writer.Write(1); + break; + case "TooLargeRecordSize": + WriteHeader(writer); + writer.Write("generation"); + writer.Write(0); + writer.Write(long.MaxValue / 2); + writer.Write(1); + break; + default: + throw new ArgumentOutOfRangeException(nameof(damage), damage, null); + } + writer.Flush(); + return stream.ToArray(); + } + + private static void WriteHeader(BinaryWriter writer) + { + writer.Write(PackageInfo.Version); + writer.Write(0L); + writer.Write(1); + writer.Write(Int32Serializer.Shared.TypeName); + writer.Write(1); + } + + private static byte[] WithUnknownType(byte[] healthy) + { + var bytes = (byte[])healthy.Clone(); + using var stream = new MemoryStream(bytes); + using var reader = new BinaryReader(stream, Encoding.UTF8); + using var writer = new BinaryWriter(stream, Encoding.UTF8); + reader.ReadString(); + reader.ReadInt64(); + var serializersCount = reader.ReadInt32(); + for (var i = 0; i < serializersCount; i++) + { + reader.ReadString(); + } + reader.ReadInt32(); + reader.ReadString(); + writer.Write(999); + writer.Flush(); + return bytes; + } + } +} diff --git a/src/Tests/Persistence/CorruptedFileBehaviourTests.cs.meta b/src/Tests/Persistence/CorruptedFileBehaviourTests.cs.meta new file mode 100644 index 0000000..dd44682 --- /dev/null +++ b/src/Tests/Persistence/CorruptedFileBehaviourTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 35db1b9068a5490d8c5eae0732e86d5a \ No newline at end of file diff --git a/src/Tests/Persistence/CorruptedLoadTests.cs b/src/Tests/Persistence/CorruptedLoadTests.cs index 33ba8b6..a319c86 100644 --- a/src/Tests/Persistence/CorruptedLoadTests.cs +++ b/src/Tests/Persistence/CorruptedLoadTests.cs @@ -145,7 +145,7 @@ public void WhenSerializerCountNegative_ThenThrowsCorruptedEvenInIgnore() writer.Write(-1); } - Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build(KeyLoadFailedBehaviour.Ignore); + Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().SetCorruptedFileBehaviour(CorruptedFileBehaviour.ThrowException).Build(KeyLoadFailedBehaviour.Ignore); action.Should().Throw(); } @@ -159,7 +159,7 @@ public void WhenHeaderTruncated_ThenThrowsCorruptedEvenInIgnore() writer.Write("1.0.0"); } - Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build(KeyLoadFailedBehaviour.Ignore); + Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().SetCorruptedFileBehaviour(CorruptedFileBehaviour.ThrowException).Build(KeyLoadFailedBehaviour.Ignore); action.Should().Throw(); } @@ -175,7 +175,7 @@ public void WhenDuplicateKey_ThenThrowsCorruptedEvenInIgnore() DuplicateFirstRecord(StoragePath); - Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build(KeyLoadFailedBehaviour.Ignore); + Action action = () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().SetCorruptedFileBehaviour(CorruptedFileBehaviour.ThrowException).Build(KeyLoadFailedBehaviour.Ignore); action.Should().Throw(); } diff --git a/src/Tests/Persistence/TruncatedEntryTests.cs b/src/Tests/Persistence/TruncatedEntryTests.cs index 2a99f24..b74c90b 100644 --- a/src/Tests/Persistence/TruncatedEntryTests.cs +++ b/src/Tests/Persistence/TruncatedEntryTests.cs @@ -72,7 +72,7 @@ public void WhenFileIsHealthy_ThenEveryEntryLoads() private Action Load() { - return () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().Build(KeyLoadFailedBehaviour.Ignore).Dispose(); + return () => BinaryStorage.Construct(StoragePath).AddPrimitiveTypes().SetCorruptedFileBehaviour(CorruptedFileBehaviour.ThrowException).Build(KeyLoadFailedBehaviour.Ignore).Dispose(); } private void WriteFile(Action write)