178 Commits
1.3.2 ... 1.6.3

Author SHA1 Message Date
Matt Nadareski
8f3a9d0ede Bump version 2025-05-12 08:19:12 -04:00
Matt Nadareski
4400f0bb87 Update to Matching 1.5.2 2025-05-12 08:18:40 -04:00
Matt Nadareski
9f12ee642d Update copyright 2024-12-30 21:22:05 -05:00
Matt Nadareski
b084b13ec3 Remove unnecessary action step 2024-12-30 21:21:45 -05:00
Matt Nadareski
93b3accbfd Ensure .NET versions are installed for testing 2024-12-19 10:50:26 -05:00
Matt Nadareski
e96c3114d9 Ensure .NET versions are installed for testing 2024-12-19 10:47:29 -05:00
Matt Nadareski
04f095018c Bump version 2024-12-16 13:59:19 -05:00
Matt Nadareski
ff18049832 Add explicit value building to extensions 2024-12-16 13:55:32 -05:00
Matt Nadareski
5e7d2901c8 Update IO to 1.5.1 2024-12-16 12:22:32 -05:00
Matt Nadareski
add7bc8ea5 Allow symbols to be packed 2024-12-16 12:20:05 -05:00
Matt Nadareski
f95853cc87 Use publish script and update README 2024-12-06 10:59:42 -05:00
Matt Nadareski
3f146d45a8 Bump version 2024-11-29 19:54:37 -05:00
Matt Nadareski
f3689087e6 Add alignment tests 2024-11-28 21:31:38 -05:00
Matt Nadareski
d2d191d86f Add boundary alignment stream extension 2024-11-28 21:17:33 -05:00
Matt Nadareski
d6cc7faea8 Bump version 2024-11-26 09:47:52 -05:00
Matt Nadareski
3b56150cc9 Fix getopts in publish script 2024-11-26 00:37:08 -05:00
Matt Nadareski
a7b50dfdf2 Fix bitstream read naming again 2024-11-25 23:12:05 -05:00
Matt Nadareski
0b7ab5b932 Add more Bitstream tests 2024-11-25 22:44:40 -05:00
Matt Nadareski
e43560cbbd Add logging tests 2024-11-25 22:33:11 -05:00
Matt Nadareski
861bfdc4f4 Update SV reader/writer and add tests 2024-11-25 22:10:41 -05:00
Matt Nadareski
933dd70654 Add INI reader/writer tests 2024-11-25 21:59:36 -05:00
Matt Nadareski
0d2a2a3b7d Update CMP reader/writer and add tests 2024-11-25 21:52:53 -05:00
Matt Nadareski
6ad4872bd4 Update PathTool and add tests 2024-11-25 21:19:45 -05:00
Matt Nadareski
8c0f54c059 Fix build for old .NET 2024-11-25 21:05:43 -05:00
Matt Nadareski
ab1b0646c4 Minor cleanup in ParentablePath 2024-11-25 21:04:09 -05:00
Matt Nadareski
450d8aab11 Update INI file and add tests 2024-11-25 21:01:52 -05:00
Matt Nadareski
ec8908aec0 Update XML writer extensions and add tests 2024-11-25 20:35:02 -05:00
Matt Nadareski
fb7ca7cde0 Update stream extensions and add tests 2024-11-25 17:09:33 -05:00
Matt Nadareski
67ca20f71b Update IO extensions and add tests 2024-11-25 16:52:49 -05:00
Matt Nadareski
ae3a27eee1 Update writer extensions and add tests 2024-11-25 16:15:12 -05:00
Matt Nadareski
3b7f910a98 Cover some bad SafeEnumerate paths 2024-11-25 15:23:51 -05:00
Matt Nadareski
244edc132f Remove unnecessary try/catch 2024-11-25 15:16:04 -05:00
Matt Nadareski
cec495d55a Update byte array extensions and add tests 2024-11-25 15:14:50 -05:00
Matt Nadareski
7656734bb2 Update reader extensions and add tests 2024-11-25 15:03:07 -05:00
Matt Nadareski
fa9310de39 Update packages 2024-11-25 13:06:17 -05:00
Matt Nadareski
fe466dfe25 Move IsNullOrEmpty from Matching 2024-11-25 11:29:22 -05:00
Matt Nadareski
d2c59c565f Make bitstream tests more robust; add notes 2024-11-18 11:26:54 -05:00
Matt Nadareski
4e221f33d5 Had it right the first time 2024-11-18 10:58:10 -05:00
Matt Nadareski
c5c8ce67ba Rename LSB/MSB to LE/BE 2024-11-18 10:30:55 -05:00
Matt Nadareski
8e3d204329 Fix LSB bit reads 2024-11-18 10:26:00 -05:00
Matt Nadareski
200d947f30 Bump version 2024-11-15 20:41:39 -05:00
Matt Nadareski
dfccdcfb05 Update Matching to 1.4.1 2024-11-15 20:40:31 -05:00
Matt Nadareski
72ff3ead48 Port extension attribute instead of framework gating 2024-11-15 20:40:01 -05:00
Matt Nadareski
fb15aecb87 Framework only matters for executable 2024-11-15 20:35:51 -05:00
Matt Nadareski
897e54ca61 Bump version 2024-11-13 00:44:44 -05:00
Matt Nadareski
3af6bc8365 Add .NET 9 to target frameworks 2024-11-13 00:44:16 -05:00
Matt Nadareski
de05bae3f8 Be smarter about framework gating 2024-11-05 21:47:43 -05:00
Matt Nadareski
97d603abb7 Update Matching to 1.3.4 2024-11-05 20:57:53 -05:00
Matt Nadareski
ed32302447 Update Matching to 1.3.3 2024-10-26 19:45:12 -04:00
Matt Nadareski
c125dc4ec0 Bump version 2024-10-24 00:22:28 -04:00
Matt Nadareski
f154ae47c0 Disable warnings, don't ignore them 2024-10-24 00:20:46 -04:00
Matt Nadareski
0d0e960b98 Use converters properly 2024-10-24 00:17:09 -04:00
Matt Nadareski
4a9f84ab66 Add LogLevel enum converters 2024-10-24 00:10:50 -04:00
Matt Nadareski
39277ee443 Add generic byte array extensions 2024-10-24 00:06:58 -04:00
Matt Nadareski
ed367ace6d Import logger from SabreTools 2024-10-24 00:04:50 -04:00
Matt Nadareski
80e72832a4 Add WORD/DWORD extensions, for fun 2024-10-15 20:52:32 -04:00
Matt Nadareski
8924a50432 Bump version 2024-10-01 13:24:53 -04:00
Matt Nadareski
97f00a2565 Update packages 2024-10-01 13:23:04 -04:00
Matt Nadareski
f35231d95b Remove Linq requirement from old .NET 2024-10-01 02:32:38 -04:00
Matt Nadareski
96c6bba93e Bump version 2024-05-13 16:19:17 -04:00
Matt Nadareski
b0d81f225b Fix thrown exception statements 2024-05-12 10:56:15 -04:00
Matt Nadareski
ef699ee1fb Bump version 2024-05-07 05:08:31 -04:00
Matt Nadareski
0910b716ba Fix build for BinaryWriter 2024-05-07 05:02:05 -04:00
Matt Nadareski
584feb33e6 Handle special struct types 2024-05-07 05:00:43 -04:00
Matt Nadareski
b99c80390e Bump version 2024-05-06 22:07:04 -04:00
Matt Nadareski
3a79646650 Handle unskippable IO errors 2024-05-02 11:37:52 -04:00
Matt Nadareski
f16f05beb9 Further wrap safe enumeration calls 2024-05-02 10:35:39 -04:00
Matt Nadareski
e901b52143 Be more explicit about InvalidOperationException 2024-05-02 10:26:55 -04:00
Matt Nadareski
a638b146b4 Handlie issues getting the enumerator 2024-05-02 10:25:53 -04:00
Matt Nadareski
6bfc961a87 Handle InvalidOperationException for SafeEnumerate 2024-05-02 10:21:41 -04:00
Matt Nadareski
a825bae039 Fix writing issues 2024-04-29 15:02:51 -04:00
Matt Nadareski
e0eba8e5bb Fix build 2024-04-29 14:50:58 -04:00
Matt Nadareski
fb4b533dfb Add type writing extensions 2024-04-29 14:45:31 -04:00
Matt Nadareski
6162af2216 Add string marshalling writer methods 2024-04-29 14:16:21 -04:00
Matt Nadareski
3b5fd128f0 Add UTF-8 and UTF-32 writing extensions 2024-04-29 14:10:20 -04:00
Matt Nadareski
9beb2177aa Add default UTF-32 null-terminated string reading 2024-04-29 12:57:37 -04:00
Matt Nadareski
f0033af712 Add short-circuiting for null-terminated UTF-8 2024-04-29 12:44:43 -04:00
Matt Nadareski
3de5b2378d Fix Unicode string reading 2024-04-29 12:43:44 -04:00
Matt Nadareski
ee7ce59627 Use safe string readers where possible 2024-04-29 12:39:02 -04:00
Matt Nadareski
39bf9c19ad Add narrow and wide reading helpers 2024-04-29 12:30:49 -04:00
Matt Nadareski
32cab49bae Clean up usings 2024-04-29 12:15:16 -04:00
Matt Nadareski
5d71957841 Slightly less verbose comments 2024-04-29 12:13:35 -04:00
Matt Nadareski
7ea182c7d8 Add marshalling helpers to ensure consistency across implementations 2024-04-29 11:58:50 -04:00
Matt Nadareski
b97ec13661 Handle LPUTF8Str implementations 2024-04-29 00:55:16 -04:00
Matt Nadareski
8caeea053f Handle LPTStr implementations 2024-04-29 00:49:45 -04:00
Matt Nadareski
a7476b6ac9 Handle TBStr implementations 2024-04-29 00:48:21 -04:00
Matt Nadareski
f0095f9e41 "marshalling" not "serialization" 2024-04-29 00:42:50 -04:00
Matt Nadareski
a0b5ea1368 Add disclaimer remarks to ReadType impelementations 2024-04-29 00:39:56 -04:00
Matt Nadareski
a94d2c8c64 Add "correct order" inheritence serialization 2024-04-29 00:36:55 -04:00
Matt Nadareski
8c19ad712a Add support for LPArray types 2024-04-28 23:47:33 -04:00
Matt Nadareski
0317f751b9 Limit current code to ByValArray 2024-04-28 23:12:48 -04:00
Matt Nadareski
b8d431b06b Handle array types properly 2024-04-28 22:58:42 -04:00
Matt Nadareski
3fcf10e2f7 Fix capitalization of TestStructStrings 2024-04-28 22:18:44 -04:00
Matt Nadareski
40e439b18c Add comprehensive strings test, fix issues 2024-04-28 20:35:18 -04:00
Matt Nadareski
bf707b1c11 Bump version 2024-04-28 19:25:33 -04:00
Matt Nadareski
d074a6a7ee Force underlying type to be used for enum 2024-04-28 19:25:12 -04:00
Matt Nadareski
0c736c2491 Bump version 2024-04-28 18:46:28 -04:00
Matt Nadareski
964506057d Handle enums like primatives 2024-04-28 17:55:54 -04:00
Matt Nadareski
cd08925411 Fix write tests, add notes 2024-04-28 16:55:16 -04:00
Matt Nadareski
6ea8aab7c7 Bump version 2024-04-28 16:43:39 -04:00
Matt Nadareski
561dbdcc9a Fix type deserialization extensions, leave some TODOs 2024-04-28 16:42:51 -04:00
Matt Nadareski
b4bad28823 Safer type reading 2024-04-28 09:41:37 -04:00
Matt Nadareski
ec9db7e732 Bump version 2024-04-26 20:49:19 -04:00
Matt Nadareski
3c7401fefc Make byte validation helper methods static 2024-04-25 20:37:16 -04:00
Matt Nadareski
09e66c9ec3 Enable type writing tests 2024-04-25 20:36:44 -04:00
Matt Nadareski
8d1bc3957c Add some more tests for BinaryReader 2024-04-25 20:29:21 -04:00
Matt Nadareski
245ca9010a Add decimal write tests 2024-04-25 20:24:46 -04:00
Matt Nadareski
88207100f1 Add decimal read tests 2024-04-25 20:19:24 -04:00
Matt Nadareski
3befd9255a Add decimal writing extensions 2024-04-25 16:36:02 -04:00
Matt Nadareski
37f2848bb2 Add U/Int24 and U/Int48 writing extensions 2024-04-25 16:13:03 -04:00
Matt Nadareski
c5dca60d28 Add Half writing implementations 2024-04-25 15:46:57 -04:00
Matt Nadareski
351e46534d Clean up duplicate write extensions 2024-04-25 15:35:19 -04:00
Matt Nadareski
d39324c887 Add Half reading implementations, add note to writers 2024-04-25 15:27:47 -04:00
Matt Nadareski
163f49281d Add U/Int48 extensions with notes 2024-04-25 15:11:58 -04:00
Matt Nadareski
03d0f7dd18 Add U/Int24 extensions 2024-04-25 15:02:48 -04:00
Matt Nadareski
fbbe77f5f2 Sanity checks before writing 2024-04-25 14:30:28 -04:00
Matt Nadareski
4bffd9d31c Use stringified characters for writing 2024-04-25 14:17:11 -04:00
Matt Nadareski
904aed1c44 Move SeekIfPossible to a better location 2024-04-25 14:11:19 -04:00
Matt Nadareski
69a41b2487 Split extensions classes 2024-04-25 14:09:34 -04:00
Matt Nadareski
f326c921e6 Rename write methods, add tests 2024-04-25 14:05:06 -04:00
Matt Nadareski
73c4e8dd50 Add writer extensions 2024-04-25 12:45:51 -04:00
Matt Nadareski
e4c8bbc3f9 Start prepping for writer extensions 2024-04-25 12:00:45 -04:00
Matt Nadareski
8df3fe2473 Bump version 2024-04-23 14:06:04 -04:00
Matt Nadareski
a2bb83ab9a Make Linux publish script executable 2024-04-23 14:05:37 -04:00
Matt Nadareski
bcb77f2de6 Add publish scripts 2024-04-23 14:05:19 -04:00
Matt Nadareski
35d4c22a20 Clean usings 2024-04-23 14:01:51 -04:00
Matt Nadareski
893cb73b0d Slight formatting update 2024-04-23 13:40:39 -04:00
Matt Nadareski
607a0375c7 Better(?) string reading extensions 2024-04-23 13:23:09 -04:00
Matt Nadareski
87d1dfe266 Add ReadType extensions 2024-04-23 10:51:54 -04:00
Matt Nadareski
d8f16b12b5 Use correct variable name in BinaryReader extensions 2024-04-23 09:52:53 -04:00
Matt Nadareski
cf08658b1e Add tests for default BinaryReader methods for consistency 2024-04-23 09:38:58 -04:00
Matt Nadareski
9547e1a355 Add decimal extensions for byte array and stream 2024-04-23 09:34:15 -04:00
Matt Nadareski
20cdb5b65e Fix summary of BinaryReaderExtensions 2024-04-22 15:25:32 -04:00
Matt Nadareski
fc10565186 Add Int128 extensions for BinaryReader 2024-04-22 15:22:14 -04:00
Matt Nadareski
c824db6b18 Add GUID tests for BinaryReader 2024-04-22 15:18:00 -04:00
Matt Nadareski
a3c26fed38 Add GUID extensions for BinaryReader 2024-04-22 15:15:02 -04:00
Matt Nadareski
2dc259d978 Reorganize binary reader extensions a bit 2024-04-22 15:13:05 -04:00
Matt Nadareski
2d950ddc54 Add float and double tests for BinaryReader extensions 2024-04-22 15:02:04 -04:00
Matt Nadareski
294c5c26df Simplify ParentablePath logic a bit 2024-04-22 01:10:02 -04:00
Matt Nadareski
dc356767ab Simplify normalization for comparison 2024-04-22 00:32:41 -04:00
Matt Nadareski
0b6c7e9885 Whitespace 2024-04-22 00:30:50 -04:00
Matt Nadareski
339da9fc16 Hah, duplicate 2024-04-22 00:22:52 -04:00
Matt Nadareski
7bc18c6952 Ensure tests aren't packable 2024-04-22 00:17:13 -04:00
Matt Nadareski
ec3afeed73 Use new enumeration wrappers 2024-04-18 16:59:25 -04:00
Matt Nadareski
658ceb5d0e Add fake Enumeration versions for net20 and net35 2024-04-18 16:57:21 -04:00
Matt Nadareski
e9a905d4a3 Use extensions in PathTool 2024-04-18 16:47:36 -04:00
Matt Nadareski
1127e96f26 Convert ListEmpty to use new safe enumerators 2024-04-18 15:34:44 -04:00
Matt Nadareski
89a8ad3703 Add safe enumeration and file enumeration 2024-04-18 15:31:38 -04:00
Matt Nadareski
ff7f7c0b8c Fix namespace for extension tests 2024-04-18 15:06:27 -04:00
Matt Nadareski
32e948653a Bump version 2024-04-18 11:59:52 -04:00
Matt Nadareski
a93da97bbd Add floating-point extensions for byte arrays and streams 2024-04-18 11:55:25 -04:00
Matt Nadareski
65bdcc563d Add BinaryReader extension tests 2024-04-18 11:44:32 -04:00
Matt Nadareski
f3c5499754 Simplify method descriptions for BinaryReader extensions 2024-04-18 11:39:24 -04:00
Matt Nadareski
02819f006b Add byte array and stream extensions tests; fix issues 2024-04-18 11:30:53 -04:00
Matt Nadareski
48512b486c Fix null terminator byte check 2024-04-18 11:06:19 -04:00
Matt Nadareski
6c289ee015 Sync byte array extensions with stream extensions 2024-04-18 11:01:28 -04:00
Matt Nadareski
973e19366a Fix ROCS test 2024-04-17 00:39:52 -04:00
Matt Nadareski
f5ef39ab76 Return 0 read bytes if overreading 2024-04-17 00:36:01 -04:00
Matt Nadareski
e4deb10db6 Bump version 2024-04-16 22:50:06 -04:00
Matt Nadareski
90d2382d5d Fix ROCS reading 2024-04-16 22:49:54 -04:00
Matt Nadareski
c48d62fd5e Bump version 2024-04-16 21:49:55 -04:00
Matt Nadareski
0f10dc2ae4 Move more classes, fix build 2024-04-16 21:47:41 -04:00
Matt Nadareski
c648ad9f5e Move extensions to new namespace 2024-04-16 21:45:26 -04:00
Matt Nadareski
cd01e170fe Port ReadOnlyBitStream from Compression, add tests 2024-04-16 20:13:27 -04:00
Matt Nadareski
ecaec1d44a Add single-stream constructor to ROCS 2024-04-16 20:02:54 -04:00
Matt Nadareski
ce521d92ca Fix test build issue 2024-04-16 13:38:08 -04:00
Matt Nadareski
a69b6dfa3a Move composite stream to new namespace 2024-04-16 13:27:23 -04:00
Matt Nadareski
18b8357cd6 Bump version 2024-04-16 11:55:38 -04:00
Matt Nadareski
c19b59dc1c Fix issues with parentable path on Linux 2024-04-16 11:50:31 -04:00
Matt Nadareski
1bdb483205 Be explicit about the characters being replaced 2024-04-16 11:18:58 -04:00
Matt Nadareski
b99f8531c1 Use string instead of array for null terminator 2024-04-16 11:12:57 -04:00
Matt Nadareski
602b951be7 Guarantee a return value for ReadBytes 2024-04-16 11:08:13 -04:00
Matt Nadareski
046bb5875a Simplify ReadBytes implementation 2024-04-16 11:05:54 -04:00
Matt Nadareski
6074fa3c3f Add U/Int128 implementations for .NET 7 and above 2024-04-16 11:03:28 -04:00
Matt Nadareski
bdad58c0dc Extract common stream read functionality 2024-04-16 10:57:17 -04:00
Matt Nadareski
4097a8cc8c Add ReadOnlyCompositeStream 2024-04-16 10:46:41 -04:00
Matt Nadareski
8e3293dd7d Bump version 2024-04-02 15:57:38 -04:00
Matt Nadareski
7ac4df8201 Add quoted string reading 2024-04-02 15:52:15 -04:00
77 changed files with 14264 additions and 1151 deletions

View File

@@ -1,4 +1,4 @@
name: Nuget Pack
name: Build and Test
on:
push:
@@ -16,31 +16,22 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build library
run: dotnet build
- name: Run tests
run: dotnet test
- name: Pack
run: dotnet pack
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: 'Nuget Package'
path: 'SabreTools.IO/bin/Release/*.nupkg'
- name: Run publish script
run: ./publish-nix.sh
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: 'SabreTools.IO/bin/Release/*.nupkg'
artifacts: "*.nupkg,*.snupkg"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True

View File

@@ -11,10 +11,13 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
- name: Run tests
run: dotnet test
run: dotnet test

View File

@@ -1,5 +1,7 @@
# SabreTools.IO
[![Build and Test](https://github.com/SabreTools/SabreTools.IO/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SabreTools/SabreTools.IO/actions/workflows/build_and_test.yml)
This library comprises I/O functionality for the following file types:
- ClrMamePro-derived Metadata files
@@ -9,3 +11,9 @@ This library comprises I/O functionality for the following file types:
There are also some extensions that are useful for wrapping common functionality required by SabreTools.
Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTools.IO).
## Releases
For the most recent stable build, download the latest release here: [Releases Page](https://github.com/SabreTools/SabreTools.IO/releases)
For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/SabreTools.IO/releases/rolling)

View File

@@ -0,0 +1,931 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class BinaryReaderExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void ReadByteArrayTest()
{
byte[] arr = new byte[4];
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.Read(arr, 0, 4);
Assert.Equal(4, read);
Assert.True(arr.SequenceEqual(_bytes.Take(4)));
}
[Fact]
public void ReadByteArrayBigEndianTest()
{
byte[] arr = new byte[4];
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadBigEndian(arr, 0, 4);
Assert.Equal(4, read);
Assert.True(arr.SequenceEqual(_bytes.Take(4).Reverse()));
}
[Fact]
public void ReadCharArrayTest()
{
char[] arr = new char[4];
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.Read(arr, 0, 4);
Assert.Equal(4, read);
Assert.True(arr.SequenceEqual(_bytes.Take(4).Select(b => (char)b)));
}
[Fact]
public void ReadCharArrayBigEndianTest()
{
char[] arr = new char[4];
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadBigEndian(arr, 0, 4);
Assert.Equal(4, read);
Assert.True(arr.SequenceEqual(_bytes.Take(4).Select(b => (char)b).Reverse()));
}
[Fact]
public void ReadByteTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
byte read = br.ReadByte();
Assert.Equal(0x00, read);
}
[Fact]
public void ReadBytesTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int length = 4;
byte[] read = br.ReadBytes(length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length)));
}
[Fact]
public void ReadBytesBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int length = 4;
byte[] read = br.ReadBytesBigEndian(length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length).Reverse()));
}
[Fact]
public void ReadCharsTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int length = 4;
char[] read = br.ReadChars(length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length).Select(b => (char)b)));
}
[Fact]
public void ReadCharsBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int length = 4;
char[] read = br.ReadCharsBigEndian(length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length).Select(b => (char)b).Reverse()));
}
[Fact]
public void ReadSByteTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
sbyte read = br.ReadSByte();
Assert.Equal(0x00, read);
}
[Fact]
public void ReadCharTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
char read = br.ReadChar();
Assert.Equal('\0', read);
}
[Fact]
public void ReadInt16Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
short read = br.ReadInt16();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadInt16BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
short read = br.ReadInt16BigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadInt16LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
short read = br.ReadInt16LittleEndian();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadUInt16();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadUInt16BigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadUInt16LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadUInt16LittleEndian();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadWORD();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadWORDBigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ushort read = br.ReadWORDLittleEndian();
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
Half expected = BitConverter.Int16BitsToHalf(0x0100);
Half read = br.ReadHalf();
Assert.Equal(expected, read);
}
[Fact]
public void ReadHalfBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
Half expected = BitConverter.Int16BitsToHalf(0x0001);
Half read = br.ReadHalfBigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt24();
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadInt24BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt24BigEndian();
Assert.Equal(0x000102, read);
}
[Fact]
public void ReadInt24LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt24LittleEndian();
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadUInt24Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt24();
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadUInt24BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt24BigEndian();
Assert.Equal((uint)0x000102, read);
}
[Fact]
public void ReadUInt24LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt24LittleEndian();
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadInt32Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt32();
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadInt32BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt32BigEndian();
Assert.Equal(0x00010203, read);
}
[Fact]
public void ReadInt32LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
int read = br.ReadInt32LittleEndian();
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadUInt32Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt32();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadUInt32BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt32BigEndian();
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadUInt32LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadUInt32LittleEndian();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadDWORD();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadDWORDBigEndian();
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadDWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
uint read = br.ReadDWORDLittleEndian();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadSingleTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
float expected = BitConverter.Int32BitsToSingle(0x03020100);
float read = br.ReadSingle();
Assert.Equal(expected, read);
}
[Fact]
public void ReadSingleBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
float expected = BitConverter.Int32BitsToSingle(0x00010203);
float read = br.ReadSingleBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt48Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt48();
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadInt48BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt48BigEndian();
Assert.Equal(0x000102030405, read);
}
[Fact]
public void ReadInt48LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt48LittleEndian();
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadUInt48Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt48();
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadUInt48BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt48BigEndian();
Assert.Equal((ulong)0x000102030405, read);
}
[Fact]
public void ReadUInt48LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt48LittleEndian();
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadInt64Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt64();
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadInt64BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt64BigEndian();
Assert.Equal(0x0001020304050607, read);
}
[Fact]
public void ReadInt64LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
long read = br.ReadInt64LittleEndian();
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadUInt64Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt64();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadUInt64BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt64BigEndian();
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadUInt64LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadUInt64LittleEndian();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadQWORD();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadQWORDBigEndian();
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadQWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
ulong read = br.ReadQWORDLittleEndian();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadDoubleTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
double expected = BitConverter.Int64BitsToDouble(0x0706050403020100);
double read = br.ReadDouble();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDoubleBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
double expected = BitConverter.Int64BitsToDouble(0x0001020304050607);
double read = br.ReadDoubleBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalTest()
{
var stream = new MemoryStream(_decimalBytes);
var br = new BinaryReader(stream);
decimal expected = 0.0123456789M;
decimal read = br.ReadDecimal();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalBigEndianTest()
{
var stream = new MemoryStream(_decimalBytes.Reverse().ToArray());
var br = new BinaryReader(stream);
decimal expected = 0.0123456789M;
decimal read = br.ReadDecimalBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var expected = new Guid(_bytes);
Guid read = br.ReadGuid();
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var expected = new Guid(_bytes.Reverse().ToArray());
Guid read = br.ReadGuidBigEndian();
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var expected = (Int128)new BigInteger(_bytes);
Int128 read = br.ReadInt128();
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt128BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var reversed = _bytes.Reverse().ToArray();
var expected = (Int128)new BigInteger(reversed);
Int128 read = br.ReadInt128BigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128Test()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var expected = (UInt128)new BigInteger(_bytes);
UInt128 read = br.ReadUInt128();
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var reversed = _bytes.Reverse().ToArray();
var expected = (UInt128)new BigInteger(reversed);
UInt128 read = br.ReadUInt128BigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
{
// Encoding.ASCII
byte[] bytes = [0x41, 0x42, 0x43, 0x00];
var stream = new MemoryStream(bytes);
var br = new BinaryReader(stream);
string? actual = br.ReadNullTerminatedString(Encoding.ASCII);
Assert.Equal("ABC", actual);
// Encoding.UTF8
bytes = [0x41, 0x42, 0x43, 0x00];
stream = new MemoryStream(bytes);
br = new BinaryReader(stream);
actual = br.ReadNullTerminatedString(Encoding.UTF8);
Assert.Equal("ABC", actual);
// Encoding.Unicode
bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00];
stream = new MemoryStream(bytes);
br = new BinaryReader(stream);
actual = br.ReadNullTerminatedString(Encoding.Unicode);
Assert.Equal("ABC", actual);
// Encoding.UTF32
bytes = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
stream = new MemoryStream(bytes);
br = new BinaryReader(stream);
actual = br.ReadNullTerminatedString(Encoding.UTF32);
Assert.Equal("ABC", actual);
// Encoding.Latin1
bytes = [0x41, 0x42, 0x43, 0x00];
stream = new MemoryStream(bytes);
br = new BinaryReader(stream);
actual = br.ReadNullTerminatedString(Encoding.Latin1);
Assert.Equal("ABC", actual);
}
[Fact]
public void ReadTypeTest()
{
// Guid
var stream = new MemoryStream(_bytes);
var br = new BinaryReader(stream);
var expectedGuid = new Guid(_bytes);
Guid actualGuid = br.ReadType<Guid>();
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = br.ReadType<Half>();
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
Int128 expectedInt128 = (Int128)new BigInteger(_bytes);
Int128 actualInt128 = br.ReadType<Int128>();
Assert.Equal(expectedHalf, actualHalf);
// UInt128
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = br.ReadType<UInt128>();
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
TestEnum expectedTestEnum = (TestEnum)0x03020100;
TestEnum actualTestEnum = br.ReadType<TestEnum>();
Assert.Equal(expectedTestEnum, actualTestEnum);
}
[Fact]
public void ReadTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(bytesWithString);
var br = new BinaryReader(stream);
var expected = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0504,
FourthValue = 0x0706,
FifthValue = "ABC",
};
var read = br.ReadType<TestStructExplicit>();
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
Assert.Equal(expected.FifthValue, read.FifthValue);
}
[Fact]
public void ReadTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(bytesWithString);
var br = new BinaryReader(stream);
var expected = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
var read = br.ReadType<TestStructSequential>();
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
Assert.Equal(expected.FifthValue, read.FifthValue);
}
[Fact]
public void ReadTypeStringsTest()
{
byte[] structBytes =
[
0x03, 0x41, 0x42, 0x43, // AnsiBStr
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
0x41, 0x42, 0x43, // ByValTStr
0x41, 0x42, 0x43, 0x00, // LPStr
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
];
var stream = new MemoryStream(structBytes);
var br = new BinaryReader(stream);
var expected = new TestStructStrings
{
AnsiBStr = "ABC",
BStr = "ABC",
ByValTStr = "ABC",
LPStr = "ABC",
LPWStr = "ABC",
};
var read = br.ReadType<TestStructStrings>();
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
Assert.Equal(expected.BStr, read.BStr);
Assert.Equal(expected.ByValTStr, read.ByValTStr);
Assert.Equal(expected.LPStr, read.LPStr);
Assert.Equal(expected.LPWStr, read.LPWStr);
}
[Fact]
public void ReadTypeArraysTest()
{
byte[] structBytes =
[
// Byte Array
0x00, 0x01, 0x02, 0x03,
// Int Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Enum Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Struct Array (X, Y)
0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0x00,
0xAA, 0x55, 0x55, 0xAA,
0x55, 0xAA, 0xAA, 0x55,
// LPArray
0x04, 0x00,
0x00, 0x01, 0x02, 0x03,
];
var stream = new MemoryStream(structBytes);
var br = new BinaryReader(stream);
var expected = new TestStructArrays
{
ByteArray = [0x00, 0x01, 0x02, 0x03],
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
EnumArray =
[
(TestEnum)0x00010203,
(TestEnum)0x01020304,
(TestEnum)0x02030405,
(TestEnum)0x03040506,
],
StructArray =
[
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
],
LPByteArrayLength = 0x0004,
LPByteArray = [0x00, 0x01, 0x02, 0x03],
};
var read = br.ReadType<TestStructArrays>();
Assert.NotNull(read.ByteArray);
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
Assert.NotNull(read.IntArray);
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
Assert.NotNull(read.EnumArray);
Assert.True(expected.EnumArray.SequenceEqual(read.EnumArray));
Assert.NotNull(read.StructArray);
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
Assert.NotNull(read.LPByteArray);
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
}
[Fact]
public void ReadTypeInheritanceTest()
{
byte[] structBytes1 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, 0xAA, 0x55, // FieldA
0x55, 0xAA, 0x55, 0xAA, // FieldB
];
var stream1 = new MemoryStream(structBytes1);
var br1 = new BinaryReader(stream1);
var expected1 = new TestStructInheritanceChild1
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA55AA,
FieldB = 0xAA55AA55,
};
var read1 = br1.ReadType<TestStructInheritanceChild1>();
Assert.NotNull(read1?.Signature);
Assert.Equal(expected1.Signature, read1.Signature);
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
Assert.Equal(expected1.FieldA, read1.FieldA);
Assert.Equal(expected1.FieldB, read1.FieldB);
byte[] structBytes2 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, // FieldA
0x55, 0xAA, // FieldB
];
var stream2 = new MemoryStream(structBytes2);
var br2 = new BinaryReader(stream2);
var expected2 = new TestStructInheritanceChild2
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA,
FieldB = 0xAA55,
};
var read2 = br2.ReadType<TestStructInheritanceChild2>();
Assert.NotNull(read2?.Signature);
Assert.Equal(expected2.Signature, read2.Signature);
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
Assert.Equal(expected2.FieldA, read2.FieldA);
Assert.Equal(expected2.FieldB, read2.FieldB);
}
}
}

View File

@@ -0,0 +1,631 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class BinaryWriterExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void WriteByteValueTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(1).ToArray();
bw.Write((byte)0x00);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteBytesTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bw.Write([0x00, 0x01, 0x02, 0x03]);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteBytesBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bw.WriteBigEndian([0x03, 0x02, 0x01, 0x00]);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSByteTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(1).ToArray();
bw.Write((sbyte)0x00);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteCharTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(1).ToArray();
bw.Write('\0');
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteCharEncodingTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x00, 0x00];
bw.Write('\0', Encoding.Unicode);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt16Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bw.Write((short)0x0100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt16BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bool write = bw.WriteBigEndian((short)0x0001);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt16Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bw.Write((ushort)0x0100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt16BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bool write = bw.WriteBigEndian((ushort)0x0001);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bw.Write(BitConverter.Int16BitsToHalf(0x0100));
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteHalfBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(2).ToArray();
bool write = bw.WriteBigEndian(BitConverter.Int16BitsToHalf(0x0001));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteInt24Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(3).ToArray();
bw.WriteAsInt24(0x020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt24BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(3).ToArray();
bool write = bw.WriteAsInt24BigEndian(0x000102);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt24Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(3).ToArray();
bw.WriteAsUInt24(0x020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt24BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(3).ToArray();
bool write = bw.WriteAsUInt24BigEndian(0x000102);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt32Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bw.Write(0x03020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt32BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bool write = bw.WriteBigEndian(0x00010203);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt32Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bw.Write((uint)0x03020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt32BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bool write = bw.WriteBigEndian((uint)0x00010203);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSingleTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bw.Write(BitConverter.Int32BitsToSingle(0x03020100));
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSingleBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(4).ToArray();
bool write = bw.WriteBigEndian(BitConverter.Int32BitsToSingle(0x00010203));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt48Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(6).ToArray();
bw.WriteAsInt48(0x050403020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt48BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(6).ToArray();
bool write = bw.WriteAsInt48BigEndian(0x000102030405);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt48Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(6).ToArray();
bw.WriteAsUInt48(0x050403020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt48BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(6).ToArray();
bool write = bw.WriteAsUInt48BigEndian(0x000102030405);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt64Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bw.Write(0x0706050403020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt64BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bool write = bw.WriteBigEndian(0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt64Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bw.Write((ulong)0x0706050403020100);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt64BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bool write = bw.WriteBigEndian((ulong)0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDoubleTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bw.Write(BitConverter.Int64BitsToDouble(0x0706050403020100));
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDoubleBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(8).ToArray();
bool write = bw.WriteBigEndian(BitConverter.Int64BitsToDouble(0x0001020304050607));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDecimalTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _decimalBytes.Take(16).ToArray();
bw.Write(0.0123456789M);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDecimalBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _decimalBytes.Take(16).Reverse().ToArray();
bool write = bw.WriteBigEndian(0.0123456789M);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteGuidTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.Write(new Guid(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteGuidBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.WriteBigEndian(new Guid(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.Write((Int128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt128BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.WriteBigEndian((Int128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt128Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.Write((UInt128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt128BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = _bytes.Take(16).ToArray();
bool write = bw.WriteBigEndian((UInt128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = bw.WriteNullTerminatedAnsiString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUTF8StringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = bw.WriteNullTerminatedUTF8String("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUnicodeStringTest()
{
var stream = new MemoryStream(new byte[8], 0, 8, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00];
bool write = bw.WriteNullTerminatedUnicodeString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUTF32StringTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
bool write = bw.WriteNullTerminatedUTF32String("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WritePrefixedAnsiStringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x03, 0x41, 0x42, 0x43];
bool write = bw.WritePrefixedAnsiString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WritePrefixedUnicodeStringTest()
{
var stream = new MemoryStream(new byte[8], 0, 8, true, true);
var bw = new BinaryWriter(stream);
byte[] expected = [0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00];
bool write = bw.WritePrefixedUnicodeString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteTypeTest()
{
// Guid
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
bool actual = bw.WriteType<Guid>(new Guid(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(new byte[2], 0, 2, true, true);
bw = new BinaryWriter(stream);
actual = bw.WriteType<Half>(BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], stream.GetBuffer());
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
bw = new BinaryWriter(stream);
actual = bw.WriteType<Int128>((Int128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
// UInt128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
bw = new BinaryWriter(stream);
actual = bw.WriteType<UInt128>((UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#endif
// Enum
stream = new MemoryStream(new byte[4], 0, 4, true, true);
bw = new BinaryWriter(stream);
actual = bw.WriteType<TestEnum>((TestEnum)0x03020100);
Assert.True(actual);
ValidateBytes([.. _bytes.Take(4)], stream.GetBuffer());
}
[Fact]
public void WriteTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var bw = new BinaryWriter(stream);
var obj = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(12).ToArray();
bool write = bw.WriteType(obj);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(new byte[24], 0, count: 24, true, true);
var bw = new BinaryWriter(stream);
var obj = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(16).ToArray();
bool write = bw.WriteType(obj);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
/// <summary>
/// Validate that a set of actual bytes matches the expected bytes
/// </summary>
private static void ValidateBytes(byte[] expected, byte[] actual)
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], actual[i]);
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Linq;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class ByteArrayExtensionsTests
{
#region Is Null or Empty
[Fact]
public void IsNullOrEmpty_Null_True()
{
byte[]? arr = null;
bool actual = arr.IsNullOrEmpty();
Assert.True(actual);
}
[Fact]
public void IsNullOrEmpty_Empty_True()
{
byte[]? arr = [];
bool actual = arr.IsNullOrEmpty();
Assert.True(actual);
}
[Fact]
public void IsNullOrEmpty_NonEmpty_False()
{
byte[]? arr = [0x01];
bool actual = arr.IsNullOrEmpty();
Assert.False(actual);
}
#endregion
#region To Hex String
[Fact]
public void ToHexString_Null()
{
byte[]? arr = null;
string? actual = arr.ToHexString();
Assert.Null(actual);
}
[Fact]
public void ToHexString_Valid()
{
byte[]? arr = [0x01, 0x02, 0x03, 0x04];
string expected = "01020304";
string? actual = arr.ToHexString();
Assert.NotNull(actual);
Assert.Equal(expected, actual);
}
#endregion
#region From Hex String
[Fact]
public void FromHexString_Null()
{
string? str = null;
byte[]? actual = str.FromHexString();
Assert.Null(actual);
}
[Fact]
public void FromHexString_Valid()
{
string str = "01020304";
byte[]? expected = [0x01, 0x02, 0x03, 0x04];
byte[]? actual = str.FromHexString();
Assert.NotNull(actual);
Assert.True(expected.SequenceEqual(actual));
}
[Fact]
public void FromHexString_Invalid()
{
string str = "0102030G";
byte[]? actual = str.FromHexString();
Assert.Null(actual);
}
#endregion
}
}

View File

@@ -0,0 +1,786 @@
using System;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class ByteArrayReaderExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void ReadByteTest()
{
int offset = 0;
byte read = _bytes.ReadByte(ref offset);
Assert.Equal(0x00, read);
}
[Fact]
public void ReadByteValueTest()
{
int offset = 0;
byte read = _bytes.ReadByteValue(ref offset);
Assert.Equal(0x00, read);
}
[Fact]
public void ReadBytesTest()
{
int offset = 0, length = 4;
byte[] read = _bytes.ReadBytes(ref offset, length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length)));
}
[Fact]
public void ReadSByteTest()
{
int offset = 0;
sbyte read = _bytes.ReadSByte(ref offset);
Assert.Equal(0x00, read);
}
[Fact]
public void ReadCharTest()
{
int offset = 0;
char read = _bytes.ReadChar(ref offset);
Assert.Equal('\0', read);
}
[Fact]
public void ReadInt16Test()
{
int offset = 0;
short read = _bytes.ReadInt16(ref offset);
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadInt16BigEndianTest()
{
int offset = 0;
short read = _bytes.ReadInt16BigEndian(ref offset);
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadInt16LittleEndianTest()
{
int offset = 0;
short read = _bytes.ReadInt16LittleEndian(ref offset);
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16Test()
{
int offset = 0;
ushort read = _bytes.ReadUInt16(ref offset);
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16BigEndianTest()
{
int offset = 0;
ushort read = _bytes.ReadUInt16BigEndian(ref offset);
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadUInt16LittleEndianTest()
{
int offset = 0;
ushort read = _bytes.ReadUInt16LittleEndian(ref offset);
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDTest()
{
int offset = 0;
ushort read = _bytes.ReadWORD(ref offset);
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDBigEndianTest()
{
int offset = 0;
ushort read = _bytes.ReadWORDBigEndian(ref offset);
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadWORDLittleEndianTest()
{
int offset = 0;
ushort read = _bytes.ReadWORDLittleEndian(ref offset);
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
int offset = 0;
Half expected = BitConverter.Int16BitsToHalf(0x0100);
Half read = _bytes.ReadHalf(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadHalfBigEndianTest()
{
int offset = 0;
Half expected = BitConverter.Int16BitsToHalf(0x0001);
Half read = _bytes.ReadHalfBigEndian(ref offset);
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
{
int offset = 0;
int read = _bytes.ReadInt24(ref offset);
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadInt24BigEndianTest()
{
int offset = 0;
int read = _bytes.ReadInt24BigEndian(ref offset);
Assert.Equal(0x000102, read);
}
[Fact]
public void ReadInt24LittleEndianTest()
{
int offset = 0;
int read = _bytes.ReadInt24LittleEndian(ref offset);
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadUInt24Test()
{
int offset = 0;
uint read = _bytes.ReadUInt24(ref offset);
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadUInt24BigEndianTest()
{
int offset = 0;
uint read = _bytes.ReadUInt24BigEndian(ref offset);
Assert.Equal((uint)0x000102, read);
}
[Fact]
public void ReadUInt24LittleEndianTest()
{
int offset = 0;
uint read = _bytes.ReadUInt24LittleEndian(ref offset);
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadInt32Test()
{
int offset = 0;
int read = _bytes.ReadInt32(ref offset);
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadInt32BigEndianTest()
{
int offset = 0;
int read = _bytes.ReadInt32BigEndian(ref offset);
Assert.Equal(0x00010203, read);
}
[Fact]
public void ReadInt32LittleEndianTest()
{
int offset = 0;
int read = _bytes.ReadInt32LittleEndian(ref offset);
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadUInt32Test()
{
int offset = 0;
uint read = _bytes.ReadUInt32(ref offset);
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadUInt32BigEndianTest()
{
int offset = 0;
uint read = _bytes.ReadUInt32BigEndian(ref offset);
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadUInt32LittleEndianTest()
{
int offset = 0;
uint read = _bytes.ReadUInt32LittleEndian(ref offset);
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDTest()
{
int offset = 0;
uint read = _bytes.ReadDWORD(ref offset);
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDBigEndianTest()
{
int offset = 0;
uint read = _bytes.ReadDWORDBigEndian(ref offset);
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadDWORDLittleEndianTest()
{
int offset = 0;
uint read = _bytes.ReadDWORDLittleEndian(ref offset);
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadSingleTest()
{
int offset = 0;
float expected = BitConverter.Int32BitsToSingle(0x03020100);
float read = _bytes.ReadSingle(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadSingleBigEndianTest()
{
int offset = 0;
float expected = BitConverter.Int32BitsToSingle(0x00010203);
float read = _bytes.ReadSingleBigEndian(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt48Test()
{
int offset = 0;
long read = _bytes.ReadInt48(ref offset);
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadInt48BigEndianTest()
{
int offset = 0;
long read = _bytes.ReadInt48BigEndian(ref offset);
Assert.Equal(0x000102030405, read);
}
[Fact]
public void ReadInt48LittleEndianTest()
{
int offset = 0;
long read = _bytes.ReadInt48LittleEndian(ref offset);
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadUInt48Test()
{
int offset = 0;
ulong read = _bytes.ReadUInt48(ref offset);
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadUInt48BigEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadUInt48BigEndian(ref offset);
Assert.Equal((ulong)0x000102030405, read);
}
[Fact]
public void ReadUInt48LittleEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadUInt48LittleEndian(ref offset);
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadInt64Test()
{
int offset = 0;
long read = _bytes.ReadInt64(ref offset);
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadInt64BigEndianTest()
{
int offset = 0;
long read = _bytes.ReadInt64BigEndian(ref offset);
Assert.Equal(0x0001020304050607, read);
}
[Fact]
public void ReadInt64LittleEndianTest()
{
int offset = 0;
long read = _bytes.ReadInt64LittleEndian(ref offset);
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadUInt64Test()
{
int offset = 0;
ulong read = _bytes.ReadUInt64(ref offset);
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadUInt64BigEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadUInt64BigEndian(ref offset);
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadUInt64LittleEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadUInt64LittleEndian(ref offset);
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDTest()
{
int offset = 0;
ulong read = _bytes.ReadQWORD(ref offset);
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDBigEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadQWORDBigEndian(ref offset);
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadQWORDLittleEndianTest()
{
int offset = 0;
ulong read = _bytes.ReadQWORDLittleEndian(ref offset);
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadDoubleTest()
{
int offset = 0;
double expected = BitConverter.Int64BitsToDouble(0x0706050403020100);
double read = _bytes.ReadDouble(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadDoubleBigEndianTest()
{
int offset = 0;
double expected = BitConverter.Int64BitsToDouble(0x0001020304050607);
double read = _bytes.ReadDoubleBigEndian(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalTest()
{
int offset = 0;
decimal expected = 0.0123456789M;
decimal read = _decimalBytes.ReadDecimal(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalBigEndianTest()
{
int offset = 0;
decimal expected = 0.0123456789M;
decimal read = _decimalBytes.Reverse().ToArray().ReadDecimalBigEndian(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidTest()
{
int offset = 0;
var expected = new Guid(_bytes);
Guid read = _bytes.ReadGuid(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidBigEndianTest()
{
int offset = 0;
var expected = new Guid(_bytes.Reverse().ToArray());
Guid read = _bytes.ReadGuidBigEndian(ref offset);
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
int offset = 0;
var expected = (Int128)new BigInteger(_bytes);
Int128 read = _bytes.ReadInt128(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt128BigEndianTest()
{
int offset = 0;
var reversed = _bytes.Reverse().ToArray();
var expected = (Int128)new BigInteger(reversed);
Int128 read = _bytes.ReadInt128BigEndian(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128Test()
{
int offset = 0;
var expected = (UInt128)new BigInteger(_bytes);
UInt128 read = _bytes.ReadUInt128(ref offset);
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128BigEndianTest()
{
int offset = 0;
var reversed = _bytes.Reverse().ToArray();
var expected = (UInt128)new BigInteger(reversed);
UInt128 read = _bytes.ReadUInt128BigEndian(ref offset);
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
{
// Encoding.ASCII
int offset = 0;
byte[] bytes = [0x41, 0x42, 0x43, 0x00];
string? actual = bytes.ReadNullTerminatedString(ref offset, Encoding.ASCII);
Assert.Equal("ABC", actual);
// Encoding.UTF8
offset = 0;
bytes = [0x41, 0x42, 0x43, 0x00];
actual = bytes.ReadNullTerminatedString(ref offset, Encoding.UTF8);
Assert.Equal("ABC", actual);
// Encoding.Unicode
offset = 0;
bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00];
actual = bytes.ReadNullTerminatedString(ref offset, Encoding.Unicode);
Assert.Equal("ABC", actual);
// Encoding.UTF32
offset = 0;
bytes = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
actual = bytes.ReadNullTerminatedString(ref offset, Encoding.UTF32);
Assert.Equal("ABC", actual);
// Encoding.Latin1
offset = 0;
bytes = [0x41, 0x42, 0x43, 0x00];
actual = bytes.ReadNullTerminatedString(ref offset, Encoding.Latin1);
Assert.Equal("ABC", actual);
}
[Fact]
public void ReadTypeTest()
{
// Guid
int offset = 0;
var expectedGuid = new Guid(_bytes);
Guid actualGuid = _bytes.ReadType<Guid>(ref offset);
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
offset = 0;
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = _bytes.ReadType<Half>(ref offset);
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
offset = 0;
Int128 expectedInt128 = (Int128)new BigInteger(_bytes);
Int128 actualInt128 = _bytes.ReadType<Int128>(ref offset);
Assert.Equal(expectedHalf, actualHalf);
// UInt128
offset = 0;
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = _bytes.ReadType<UInt128>(ref offset);
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
offset = 0;
TestEnum expectedTestEnum = (TestEnum)0x03020100;
TestEnum actualTestEnum = _bytes.ReadType<TestEnum>(ref offset);
Assert.Equal(expectedTestEnum, actualTestEnum);
}
[Fact]
public void ReadTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
int offset = 0;
var expected = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0504,
FourthValue = 0x0706,
FifthValue = "ABC",
};
var read = bytesWithString.ReadType<TestStructExplicit>(ref offset);
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
}
[Fact]
public void ReadTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
int offset = 0;
var expected = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
var read = bytesWithString.ReadType<TestStructSequential>(ref offset);
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
Assert.Equal(expected.FifthValue, read.FifthValue);
}
[Fact]
public void ReadTypeStringsTest()
{
byte[] structBytes =
[
0x03, 0x41, 0x42, 0x43, // AnsiBStr
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
0x41, 0x42, 0x43, // ByValTStr
0x41, 0x42, 0x43, 0x00, // LPStr
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
];
int offset = 0;
var expected = new TestStructStrings
{
AnsiBStr = "ABC",
BStr = "ABC",
ByValTStr = "ABC",
LPStr = "ABC",
LPWStr = "ABC",
};
var read = structBytes.ReadType<TestStructStrings>(ref offset);
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
Assert.Equal(expected.BStr, read.BStr);
Assert.Equal(expected.ByValTStr, read.ByValTStr);
Assert.Equal(expected.LPStr, read.LPStr);
Assert.Equal(expected.LPWStr, read.LPWStr);
}
[Fact]
public void ReadTypeArraysTest()
{
byte[] structBytes =
[
// Byte Array
0x00, 0x01, 0x02, 0x03,
// Int Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Enum Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Struct Array (X, Y)
0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0x00,
0xAA, 0x55, 0x55, 0xAA,
0x55, 0xAA, 0xAA, 0x55,
// LPArray
0x04, 0x00,
0x00, 0x01, 0x02, 0x03,
];
int offset = 0;
var expected = new TestStructArrays
{
ByteArray = [0x00, 0x01, 0x02, 0x03],
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
EnumArray =
[
(TestEnum)0x00010203,
(TestEnum)0x01020304,
(TestEnum)0x02030405,
(TestEnum)0x03040506,
],
StructArray =
[
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
],
LPByteArrayLength = 0x0004,
LPByteArray = [0x00, 0x01, 0x02, 0x03],
};
var read = structBytes.ReadType<TestStructArrays>(ref offset);
Assert.NotNull(read.ByteArray);
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
Assert.NotNull(read.IntArray);
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
Assert.NotNull(read.EnumArray);
Assert.True(expected.EnumArray.SequenceEqual(read.EnumArray));
Assert.NotNull(read.StructArray);
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
Assert.NotNull(read.LPByteArray);
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
}
[Fact]
public void ReadTypeInheritanceTest()
{
byte[] structBytes1 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, 0xAA, 0x55, // FieldA
0x55, 0xAA, 0x55, 0xAA, // FieldB
];
int offset1 = 0;
var expected1 = new TestStructInheritanceChild1
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA55AA,
FieldB = 0xAA55AA55,
};
var read1 = structBytes1.ReadType<TestStructInheritanceChild1>(ref offset1);
Assert.NotNull(read1?.Signature);
Assert.Equal(expected1.Signature, read1.Signature);
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
Assert.Equal(expected1.FieldA, read1.FieldA);
Assert.Equal(expected1.FieldB, read1.FieldB);
byte[] structBytes2 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, // FieldA
0x55, 0xAA, // FieldB
];
int offset2 = 0;
var expected2 = new TestStructInheritanceChild2
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA,
FieldB = 0xAA55,
};
var read2 = structBytes2.ReadType<TestStructInheritanceChild2>(ref offset2);
Assert.NotNull(read2?.Signature);
Assert.Equal(expected2.Signature, read2.Signature);
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
Assert.Equal(expected2.FieldA, read2.FieldA);
Assert.Equal(expected2.FieldB, read2.FieldB);
}
}
}

View File

@@ -0,0 +1,650 @@
using System;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class ByteArrayWriterExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void WriteByteTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(1).ToArray();
bool write = buffer.Write(ref offset, (byte)0x00);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteBytesTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.Write(ref offset, [0x00, 0x01, 0x02, 0x03]);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteBytesBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.WriteBigEndian(ref offset, [0x03, 0x02, 0x01, 0x00]);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteSByteTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(1).ToArray();
bool write = buffer.Write(ref offset, (sbyte)0x00);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteCharTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(1).ToArray();
bool write = buffer.Write(ref offset, '\0');
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteCharEncodingTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = [0x00, 0x00];
bool write = buffer.Write(ref offset, '\0', Encoding.Unicode);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt16Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.Write(ref offset, (short)0x0100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt16BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (short)0x0001);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt16Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.Write(ref offset, (ushort)0x0100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt16BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (ushort)0x0001);
Assert.True(write);
ValidateBytes(expected, buffer);
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.Write(ref offset, BitConverter.Int16BitsToHalf(0x0100));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteHalfBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(2).ToArray();
bool write = buffer.WriteBigEndian(ref offset, BitConverter.Int16BitsToHalf(0x0001));
Assert.True(write);
ValidateBytes(expected, buffer);
}
#endif
[Fact]
public void WriteInt24Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(3).ToArray();
bool write = buffer.WriteAsInt24(ref offset, 0x020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt24BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(3).ToArray();
bool write = buffer.WriteAsInt24BigEndian(ref offset, 0x000102);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt24Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(3).ToArray();
bool write = buffer.WriteAsUInt24(ref offset, 0x020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt24BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(3).ToArray();
bool write = buffer.WriteAsUInt24BigEndian(ref offset, 0x000102);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt32Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.Write(ref offset, 0x03020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt32BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.WriteBigEndian(ref offset, 0x00010203);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt32Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.Write(ref offset, (uint)0x03020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt32BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (uint)0x00010203);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteSingleTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.Write(ref offset, BitConverter.Int32BitsToSingle(0x03020100));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteSingleBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(4).ToArray();
bool write = buffer.WriteBigEndian(ref offset, BitConverter.Int32BitsToSingle(0x00010203));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt48Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(6).ToArray();
bool write = buffer.WriteAsInt48(ref offset, 0x050403020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt48BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(6).ToArray();
bool write = buffer.WriteAsInt48BigEndian(ref offset, 0x000102030405);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt48Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(6).ToArray();
bool write = buffer.WriteAsUInt48(ref offset, 0x050403020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt48BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(6).ToArray();
bool write = buffer.WriteAsUInt48BigEndian(ref offset, 0x000102030405);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt64Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.Write(ref offset, 0x0706050403020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt64BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.WriteBigEndian(ref offset, 0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt64Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.Write(ref offset, (ulong)0x0706050403020100);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt64BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (ulong)0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteDoubleTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.Write(ref offset, BitConverter.Int64BitsToDouble(0x0706050403020100));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteDoubleBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(8).ToArray();
bool write = buffer.WriteBigEndian(ref offset, BitConverter.Int64BitsToDouble(0x0001020304050607));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteDecimalTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _decimalBytes.Take(16).ToArray();
bool write = buffer.Write(ref offset, 0.0123456789M);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteDecimalBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _decimalBytes.Take(16).Reverse().ToArray();
bool write = buffer.WriteBigEndian(ref offset, 0.0123456789M);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteGuidTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.Write(ref offset, new Guid(_bytes));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteGuidBigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.WriteBigEndian(ref offset, new Guid(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, buffer);
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.Write(ref offset, (Int128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteInt128BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (Int128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt128Test()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.Write(ref offset, (UInt128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteUInt128BigEndianTest()
{
byte[] buffer = new byte[16];
int offset = 0;
byte[] expected = _bytes.Take(16).ToArray();
bool write = buffer.WriteBigEndian(ref offset, (UInt128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, buffer);
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
{
int offset = 0;
byte[] buffer = new byte[4];
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = buffer.WriteNullTerminatedAnsiString(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteNullTerminatedUTF8StringTest()
{
int offset = 0;
byte[] buffer = new byte[4];
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = buffer.WriteNullTerminatedUTF8String(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteNullTerminatedUnicodeStringTest()
{
int offset = 0;
byte[] buffer = new byte[8];
byte[] expected = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00];
bool write = buffer.WriteNullTerminatedUnicodeString(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteNullTerminatedUTF32StringTest()
{
int offset = 0;
byte[] buffer = new byte[16];
byte[] expected = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
bool write = buffer.WriteNullTerminatedUTF32String(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WritePrefixedAnsiStringTest()
{
int offset = 0;
byte[] buffer = new byte[4];
byte[] expected = [0x03, 0x41, 0x42, 0x43];
bool write = buffer.WritePrefixedAnsiString(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WritePrefixedUnicodeStringTest()
{
int offset = 0;
byte[] buffer = new byte[8];
byte[] expected = [0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00];
bool write = buffer.WritePrefixedUnicodeString(ref offset, "ABC");
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteTypeTest()
{
// Guid
int offset = 0;
byte[] buffer = new byte[16];
bool actual = buffer.WriteType<Guid>(ref offset, new Guid(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, buffer);
#if NET6_0_OR_GREATER
// Half
offset = 0;
buffer = new byte[2];
actual = buffer.WriteType<Half>(ref offset, BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], buffer);
#endif
#if NET7_0_OR_GREATER
// Int128
offset = 0;
buffer = new byte[16];
actual = buffer.WriteType<Int128>(ref offset, (Int128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, buffer);
// UInt128
offset = 0;
buffer = new byte[16];
actual = buffer.WriteType<UInt128>(ref offset, (UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, buffer);
#endif
// Enum
offset = 0;
buffer = new byte[4];
actual = buffer.WriteType<TestEnum>(ref offset, (TestEnum)0x03020100);
Assert.True(actual);
ValidateBytes([.. _bytes.Take(4)], buffer);
}
[Fact]
public void WriteTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
byte[] buffer = new byte[16];
int offset = 0;
var obj = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(12).ToArray();
bool write = buffer.WriteType(ref offset, obj);
Assert.True(write);
ValidateBytes(expected, buffer);
}
[Fact]
public void WriteTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
byte[] buffer = new byte[24];
int offset = 0;
var obj = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(16).ToArray();
bool write = buffer.WriteType(ref offset, obj);
Assert.True(write);
ValidateBytes(expected, buffer);
}
/// <summary>
/// Validate that a set of actual bytes matches the expected bytes
/// </summary>
private static void ValidateBytes(byte[] expected, byte[] actual)
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], actual[i]);
}
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class EnumerableExtensionsTests
{
[Fact]
public void SafeEnumerate_Empty()
{
var source = Enumerable.Empty<string>();
var safe = source.SafeEnumerate();
var list = safe.ToList();
Assert.Empty(list);
}
[Fact]
public void SafeEnumerate_Throws()
{
var source = new List<string> { "a", "ab", "abc" };
var wrapper = new ThrowsEnumerable(source);
var safe = wrapper.SafeEnumerate();
var list = safe.ToList();
Assert.Empty(list);
}
[Fact]
public void SafeEnumerate_NoError()
{
var source = new List<string> { "a", "ab", "abc" };
var safe = source.SafeEnumerate();
var list = safe.ToList();
Assert.Equal(3, list.Count);
}
[Fact]
public void SafeEnumerate_ErrorMid()
{
var source = new List<string> { "a", "ab", "abc" };
var wrapper = new ErrorEnumerable(source);
var safe = wrapper.SafeEnumerate();
var list = safe.ToList();
Assert.Equal(2, list.Count);
}
[Fact]
public void SafeEnumerate_ErrorLast()
{
var source = new List<string> { "a", "ab", "abc", "abcd" };
var wrapper = new ErrorEnumerable(source);
var safe = wrapper.SafeEnumerate();
var list = safe.ToList();
Assert.Equal(2, list.Count);
}
/// <summary>
/// Fake enumerable that uses <see cref="ErrorEnumerator"/>
/// </summary>
private class ErrorEnumerable : IEnumerable<string>
{
/// <summary>
/// Enumerator to use during enumeration
/// </summary>
private readonly ErrorEnumerator _enumerator;
public ErrorEnumerable(IEnumerable<string> source)
{
_enumerator = new ErrorEnumerator(source);
}
/// <inheritdoc/>
public IEnumerator<string> GetEnumerator() => _enumerator;
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
}
/// <summary>
/// Fake enumerator that throws an exception every other item while moving to the next item
/// </summary>
private class ErrorEnumerator : IEnumerator<string>
{
/// <inheritdoc/>
public string Current
{
get
{
if (_index == -1)
throw new InvalidOperationException();
return _enumerator.Current;
}
}
/// <inheritdoc/>
object IEnumerator.Current => Current;
/// <summary>
/// Enumerator from the source enumerable
/// </summary>
private readonly IEnumerator<string> _enumerator;
/// <summary>
/// Enumerators start before the data
/// </summary>
private int _index = -1;
public ErrorEnumerator(IEnumerable<string> source)
{
_enumerator = source.GetEnumerator();
}
/// <inheritdoc/>
public void Dispose() { }
/// <inheritdoc/>
public bool MoveNext()
{
// Move to the next item, if possible
bool moved = _enumerator.MoveNext();
if (!moved)
return false;
// Get the next real item
_index++;
// Every other move, throw an exception
if (_index % 2 == 1)
throw new Exception("Access issue for this item in the enumerable");
return true;
}
/// <inheritdoc/>
public void Reset()
{
_enumerator.Reset();
_index = -1;
}
}
/// <summary>
/// Fake enumerable that throws an exception for the enumerator
/// </summary>
private class ThrowsEnumerable : IEnumerable<string>
{
public ThrowsEnumerable(IEnumerable<string> source) { }
/// <inheritdoc/>
public IEnumerator<string> GetEnumerator() => throw new Exception();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => throw new Exception();
}
}
}

View File

@@ -0,0 +1,312 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class IOExtensionsTests
{
#region Ensure
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", " ")] // TODO: This is a bad result
[InlineData("dirname", "dirname")]
[InlineData("\"dirname\"", "dirname")]
public void EnsureTest(string? dir, string? expected)
{
// Handle test setup
expected ??= PathTool.GetRuntimeDirectory();
if (expected != null)
expected = Path.GetFullPath(expected);
string actual = dir.Ensure(create: false);
Assert.Equal(expected, actual);
}
#endregion
#region Get Encoding
[Fact]
public void GetEncoding_EmptyPath()
{
string path = "";
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_InvalidPath()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "INVALID");
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
// Disable warning about UTF7 usage
#pragma warning disable SYSLIB0001
[Fact]
public void GetEncoding_UTF7()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "utf7bom.txt");
Encoding expected = Encoding.UTF7;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
#pragma warning restore SYSLIB0001
[Fact]
public void GetEncoding_UTF8()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "utf8bom.txt");
Encoding expected = Encoding.UTF8;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_Unicode()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "utf16lebom.txt");
Encoding expected = Encoding.Unicode;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_BigEndianUnicode()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "utf16bebom.txt");
Encoding expected = Encoding.BigEndianUnicode;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_UTF32()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "utf32bom.txt");
Encoding expected = Encoding.UTF32;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_ASCII()
{
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt");
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
#endregion
#region Get Normalized Extension
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData("no-extension", null)]
[InlineData("NO-EXTENSION", null)]
[InlineData("no-extension.", null)]
[InlineData("NO-EXTENSION.", null)]
[InlineData("filename.ext", "ext")]
[InlineData("FILENAME.EXT", "ext")]
public void GetNormalizedExtensionTest(string? path, string? expected)
{
string? actual = path.GetNormalizedExtension();
Assert.Equal(expected, actual);
}
#endregion
#region Path
[Fact]
public void ListEmpty_NullDirectory()
{
string? dir = null;
var empty = dir.ListEmpty();
Assert.Null(empty);
}
[Fact]
public void ListEmpty_InvalidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "INVALID");
var empty = dir.ListEmpty();
Assert.Null(empty);
}
[Fact]
public void ListEmpty_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var empty = dir.ListEmpty();
Assert.NotNull(empty);
Assert.Empty(empty);
}
[Fact]
public void SafeGetDirectories_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeGetDirectories();
Assert.Single(dirs);
}
[Fact]
public void SafeGetDirectories_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeGetDirectories("*");
Assert.Single(dirs);
}
[Fact]
public void SafeGetDirectories_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeGetDirectories("*", SearchOption.AllDirectories);
Assert.Single(dirs);
}
[Fact]
public void SafeGetFiles_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeGetFiles();
Assert.NotEmpty(files);
}
[Fact]
public void SafeGetFiles_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeGetFiles("*");
Assert.NotEmpty(files);
}
[Fact]
public void SafeGetFiles_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeGetFiles("*", SearchOption.AllDirectories);
Assert.NotEmpty(files);
}
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeGetFileSystemEntries();
Assert.NotEmpty(entries);
}
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeGetFileSystemEntries("*");
Assert.NotEmpty(entries);
}
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeGetFileSystemEntries("*", SearchOption.AllDirectories);
Assert.NotEmpty(entries);
}
[Fact]
public void SafeEnumerateDirectories_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeEnumerateDirectories();
Assert.Single(dirs);
}
[Fact]
public void SafeEnumerateDirectories_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeEnumerateDirectories("*");
Assert.Single(dirs);
}
[Fact]
public void SafeEnumerateDirectories_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var dirs = dir.SafeEnumerateDirectories("*", SearchOption.AllDirectories);
Assert.Single(dirs);
}
[Fact]
public void SafeEnumerateFiles_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeEnumerateFiles();
Assert.NotEmpty(files);
}
[Fact]
public void SafeEnumerateFiles_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeEnumerateFiles("*");
Assert.NotEmpty(files);
}
[Fact]
public void SafeEnumerateFiles_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeEnumerateFiles("*", SearchOption.AllDirectories);
Assert.NotEmpty(files);
}
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeEnumerateFileSystemEntries();
Assert.NotEmpty(entries);
}
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory_Pattern()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeEnumerateFileSystemEntries("*");
Assert.NotEmpty(entries);
}
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory_PatternOption()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var entries = dir.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories);
Assert.NotEmpty(entries);
}
#endregion
}
}

View File

@@ -0,0 +1,232 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class StreamExtensionsTests
{
#region Align to Boundary
[Fact]
public void AlignToBoundary_Null_False()
{
Stream? stream = null;
byte alignment = 4;
bool actual = stream.AlignToBoundary(alignment);
Assert.False(actual);
}
[Fact]
public void AlignToBoundary_Empty_False()
{
Stream? stream = new MemoryStream([]);
byte alignment = 4;
bool actual = stream.AlignToBoundary(alignment);
Assert.False(actual);
}
[Fact]
public void AlignToBoundary_EOF_False()
{
Stream? stream = new MemoryStream([0x01, 0x02]);
byte alignment = 4;
stream.Position = 1;
bool actual = stream.AlignToBoundary(alignment);
Assert.False(actual);
}
[Fact]
public void AlignToBoundary_TooShort_False()
{
Stream? stream = new MemoryStream([0x01, 0x02]);
byte alignment = 4;
stream.Position = 1;
bool actual = stream.AlignToBoundary(alignment);
Assert.False(actual);
}
[Fact]
public void AlignToBoundary_CanAlign_True()
{
Stream? stream = new MemoryStream([0x01, 0x02, 0x03, 0x04, 0x05]);
byte alignment = 4;
stream.Position = 1;
bool actual = stream.AlignToBoundary(alignment);
Assert.True(actual);
}
#endregion
#region Seek If Possible
[Fact]
public void SeekIfPossible_NonSeekable_CurrentPosition()
{
var stream = new NonSeekableStream();
long actual = stream.SeekIfPossible(0);
Assert.Equal(8, actual);
}
[Fact]
public void SeekIfPossible_NonPositionable_InvalidPosition()
{
var stream = new NonPositionableStream();
long actual = stream.SeekIfPossible(0);
Assert.Equal(-1, actual);
}
[Fact]
public void SeekIfPossible_HiddenNonSeekable_InvalidPosition()
{
var stream = new HiddenNonSeekableStream();
long actual = stream.SeekIfPossible(0);
Assert.Equal(-1, actual);
}
[Fact]
public void SeekIfPossible_NonNegative_ValidPosition()
{
var stream = new MemoryStream(new byte[16], 0, 16, false, true);
long actual = stream.SeekIfPossible(5);
Assert.Equal(5, actual);
}
[Fact]
public void SeekIfPossible_Negative_ValidPosition()
{
var stream = new MemoryStream(new byte[16], 0, 16, false, true);
long actual = stream.SeekIfPossible(-3);
Assert.Equal(13, actual);
}
#endregion
/// <summary>
/// Represents a hidden non-seekable stream
/// </summary>
private class HiddenNonSeekableStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => true;
public override long Length => 16;
public override long Position { get => 8; set => throw new NotSupportedException(); }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Represents a non-seekable stream
/// </summary>
private class NonSeekableStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => 16;
public override long Position { get => 8; set => throw new NotSupportedException(); }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Represents a non-seekable, non-positionable stream
/// </summary>
private class NonPositionableStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => 16;
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
}
}

View File

@@ -0,0 +1,790 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class StreamReaderExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void ReadByteArrayTest()
{
byte[] arr = new byte[4];
var stream = new MemoryStream(_bytes);
int read = stream.Read(arr, 0, 4);
Assert.Equal(4, read);
Assert.True(arr.SequenceEqual(_bytes.Take(4)));
}
[Fact]
public void ReadByteValueTest()
{
var stream = new MemoryStream(_bytes);
byte read = stream.ReadByteValue();
Assert.Equal(0x00, read);
}
[Fact]
public void ReadBytesTest()
{
var stream = new MemoryStream(_bytes);
int length = 4;
byte[] read = stream.ReadBytes(length);
Assert.Equal(length, read.Length);
Assert.True(read.SequenceEqual(_bytes.Take(length)));
}
[Fact]
public void ReadSByteTest()
{
var stream = new MemoryStream(_bytes);
sbyte read = stream.ReadSByte();
Assert.Equal(0x00, read);
}
[Fact]
public void ReadCharTest()
{
var stream = new MemoryStream(_bytes);
char read = stream.ReadChar();
Assert.Equal('\0', read);
}
[Fact]
public void ReadInt16Test()
{
var stream = new MemoryStream(_bytes);
short read = stream.ReadInt16();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadInt16BigEndianTest()
{
var stream = new MemoryStream(_bytes);
short read = stream.ReadInt16BigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadInt16LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
short read = stream.ReadInt16LittleEndian();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16Test()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadUInt16();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadUInt16BigEndianTest()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadUInt16BigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadUInt16LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadUInt16LittleEndian();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDTest()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadWORD();
Assert.Equal(0x0100, read);
}
[Fact]
public void ReadWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadWORDBigEndian();
Assert.Equal(0x0001, read);
}
[Fact]
public void ReadWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
ushort read = stream.ReadWORDLittleEndian();
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
var stream = new MemoryStream(_bytes);
Half expected = BitConverter.Int16BitsToHalf(0x0100);
Half read = stream.ReadHalf();
Assert.Equal(expected, read);
}
[Fact]
public void ReadHalfBigEndianTest()
{
var stream = new MemoryStream(_bytes);
Half expected = BitConverter.Int16BitsToHalf(0x0001);
Half read = stream.ReadHalfBigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt24();
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadInt24BigEndianTest()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt24BigEndian();
Assert.Equal(0x000102, read);
}
[Fact]
public void ReadInt24LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt24LittleEndian();
Assert.Equal(0x020100, read);
}
[Fact]
public void ReadUInt24Test()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt24();
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadUInt24BigEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt24BigEndian();
Assert.Equal((uint)0x000102, read);
}
[Fact]
public void ReadUInt24LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt24LittleEndian();
Assert.Equal((uint)0x020100, read);
}
[Fact]
public void ReadInt32Test()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt32();
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadInt32BigEndianTest()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt32BigEndian();
Assert.Equal(0x00010203, read);
}
[Fact]
public void ReadInt32LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
int read = stream.ReadInt32LittleEndian();
Assert.Equal(0x03020100, read);
}
[Fact]
public void ReadUInt32Test()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt32();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadUInt32BigEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt32BigEndian();
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadUInt32LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadUInt32LittleEndian();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadDWORD();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadDWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadDWORDBigEndian();
Assert.Equal((uint)0x00010203, read);
}
[Fact]
public void ReadDWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
uint read = stream.ReadDWORDLittleEndian();
Assert.Equal((uint)0x03020100, read);
}
[Fact]
public void ReadSingleTest()
{
var stream = new MemoryStream(_bytes);
float expected = BitConverter.Int32BitsToSingle(0x03020100);
float read = stream.ReadSingle();
Assert.Equal(expected, read);
}
[Fact]
public void ReadSingleBigEndianTest()
{
var stream = new MemoryStream(_bytes);
float expected = BitConverter.Int32BitsToSingle(0x00010203);
float read = stream.ReadSingleBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt48Test()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt48();
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadInt48BigEndianTest()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt48BigEndian();
Assert.Equal(0x000102030405, read);
}
[Fact]
public void ReadInt48LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt48LittleEndian();
Assert.Equal(0x050403020100, read);
}
[Fact]
public void ReadUInt48Test()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt48();
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadUInt48BigEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt48BigEndian();
Assert.Equal((ulong)0x000102030405, read);
}
[Fact]
public void ReadUInt48LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt48LittleEndian();
Assert.Equal((ulong)0x050403020100, read);
}
[Fact]
public void ReadInt64Test()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt64();
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadInt64BigEndianTest()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt64BigEndian();
Assert.Equal(0x0001020304050607, read);
}
[Fact]
public void ReadInt64LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
long read = stream.ReadInt64LittleEndian();
Assert.Equal(0x0706050403020100, read);
}
[Fact]
public void ReadUInt64Test()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt64();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadUInt64BigEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt64BigEndian();
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadUInt64LittleEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadUInt64LittleEndian();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadQWORD();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadQWORDBigEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadQWORDBigEndian();
Assert.Equal((ulong)0x0001020304050607, read);
}
[Fact]
public void ReadQWORDLittleEndianTest()
{
var stream = new MemoryStream(_bytes);
ulong read = stream.ReadQWORDLittleEndian();
Assert.Equal((ulong)0x0706050403020100, read);
}
[Fact]
public void ReadDoubleTest()
{
var stream = new MemoryStream(_bytes);
double expected = BitConverter.Int64BitsToDouble(0x0706050403020100);
double read = stream.ReadDouble();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDoubleBigEndianTest()
{
var stream = new MemoryStream(_bytes);
double expected = BitConverter.Int64BitsToDouble(0x0001020304050607);
double read = stream.ReadDoubleBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalTest()
{
var stream = new MemoryStream(_decimalBytes);
decimal expected = 0.0123456789M;
decimal read = stream.ReadDecimal();
Assert.Equal(expected, read);
}
[Fact]
public void ReadDecimalBigEndianTest()
{
var stream = new MemoryStream(_decimalBytes.Reverse().ToArray());
decimal expected = 0.0123456789M;
decimal read = stream.ReadDecimalBigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidTest()
{
var stream = new MemoryStream(_bytes);
var expected = new Guid(_bytes);
Guid read = stream.ReadGuid();
Assert.Equal(expected, read);
}
[Fact]
public void ReadGuidBigEndianTest()
{
var stream = new MemoryStream(_bytes);
var expected = new Guid(_bytes.Reverse().ToArray());
Guid read = stream.ReadGuidBigEndian();
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
var stream = new MemoryStream(_bytes);
var expected = (Int128)new BigInteger(_bytes);
Int128 read = stream.ReadInt128();
Assert.Equal(expected, read);
}
[Fact]
public void ReadInt128BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var reversed = _bytes.Reverse().ToArray();
var expected = (Int128)new BigInteger(reversed);
Int128 read = stream.ReadInt128BigEndian();
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128Test()
{
var stream = new MemoryStream(_bytes);
var expected = (UInt128)new BigInteger(_bytes);
UInt128 read = stream.ReadUInt128();
Assert.Equal(expected, read);
}
[Fact]
public void ReadUInt128BigEndianTest()
{
var stream = new MemoryStream(_bytes);
var reversed = _bytes.Reverse().ToArray();
var expected = (UInt128)new BigInteger(reversed);
UInt128 read = stream.ReadUInt128BigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
{
// Encoding.ASCII
byte[] bytes = [0x41, 0x42, 0x43, 0x00];
var stream = new MemoryStream(bytes);
string? actual = stream.ReadNullTerminatedString(Encoding.ASCII);
Assert.Equal("ABC", actual);
// Encoding.UTF8
bytes = [0x41, 0x42, 0x43, 0x00];
stream = new MemoryStream(bytes);
actual = stream.ReadNullTerminatedString(Encoding.UTF8);
Assert.Equal("ABC", actual);
// Encoding.Unicode
bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00];
stream = new MemoryStream(bytes);
actual = stream.ReadNullTerminatedString(Encoding.Unicode);
Assert.Equal("ABC", actual);
// Encoding.UTF32
bytes = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
stream = new MemoryStream(bytes);
actual = stream.ReadNullTerminatedString(Encoding.UTF32);
Assert.Equal("ABC", actual);
// Encoding.Latin1
bytes = [0x41, 0x42, 0x43, 0x00];
stream = new MemoryStream(bytes);
actual = stream.ReadNullTerminatedString(Encoding.Latin1);
Assert.Equal("ABC", actual);
}
[Fact]
public void ReadTypeTest()
{
// Guid
var stream = new MemoryStream(_bytes);
var expectedGuid = new Guid(_bytes);
Guid actualGuid = stream.ReadType<Guid>();
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(_bytes);
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = stream.ReadType<Half>();
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(_bytes);
Int128 expectedInt128 = (Int128)new BigInteger(_bytes);
Int128 actualInt128 = stream.ReadType<Int128>();
Assert.Equal(expectedHalf, actualHalf);
// UInt128
stream = new MemoryStream(_bytes);
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = stream.ReadType<UInt128>();
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
stream = new MemoryStream(_bytes);
TestEnum expectedTestEnum = (TestEnum)0x03020100;
TestEnum actualTestEnum = stream.ReadType<TestEnum>();
Assert.Equal(expectedTestEnum, actualTestEnum);
}
[Fact]
public void ReadTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(bytesWithString);
var expected = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0504,
FourthValue = 0x0706,
FifthValue = "ABC",
};
var read = stream.ReadType<TestStructExplicit>();
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
}
[Fact]
public void ReadTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(bytesWithString);
var expected = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
var read = stream.ReadType<TestStructSequential>();
Assert.Equal(expected.FirstValue, read.FirstValue);
Assert.Equal(expected.SecondValue, read.SecondValue);
Assert.Equal(expected.ThirdValue, read.ThirdValue);
Assert.Equal(expected.FourthValue, read.FourthValue);
Assert.Equal(expected.FifthValue, read.FifthValue);
}
[Fact]
public void ReadTypeStringsTest()
{
byte[] structBytes =
[
0x03, 0x41, 0x42, 0x43, // AnsiBStr
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
0x41, 0x42, 0x43, // ByValTStr
0x41, 0x42, 0x43, 0x00, // LPStr
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
];
var stream = new MemoryStream(structBytes);
var expected = new TestStructStrings
{
AnsiBStr = "ABC",
BStr = "ABC",
ByValTStr = "ABC",
LPStr = "ABC",
LPWStr = "ABC",
};
var read = stream.ReadType<TestStructStrings>();
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
Assert.Equal(expected.BStr, read.BStr);
Assert.Equal(expected.ByValTStr, read.ByValTStr);
Assert.Equal(expected.LPStr, read.LPStr);
Assert.Equal(expected.LPWStr, read.LPWStr);
}
[Fact]
public void ReadTypeArraysTest()
{
byte[] structBytes =
[
// Byte Array
0x00, 0x01, 0x02, 0x03,
// Int Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Enum Array
0x03, 0x02, 0x01, 0x00,
0x04, 0x03, 0x02, 0x01,
0x05, 0x04, 0x03, 0x02,
0x06, 0x05, 0x04, 0x03,
// Struct Array (X, Y)
0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0x00,
0xAA, 0x55, 0x55, 0xAA,
0x55, 0xAA, 0xAA, 0x55,
// LPArray
0x04, 0x00,
0x00, 0x01, 0x02, 0x03,
];
var stream = new MemoryStream(structBytes);
var expected = new TestStructArrays
{
ByteArray = [0x00, 0x01, 0x02, 0x03],
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
EnumArray =
[
(TestEnum)0x00010203,
(TestEnum)0x01020304,
(TestEnum)0x02030405,
(TestEnum)0x03040506,
],
StructArray =
[
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
],
LPByteArrayLength = 0x0004,
LPByteArray = [0x00, 0x01, 0x02, 0x03],
};
var read = stream.ReadType<TestStructArrays>();
Assert.NotNull(read.ByteArray);
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
Assert.NotNull(read.IntArray);
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
Assert.NotNull(read.EnumArray);
Assert.True(expected.EnumArray.SequenceEqual(read.EnumArray));
Assert.NotNull(read.StructArray);
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
Assert.NotNull(read.LPByteArray);
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
}
[Fact]
public void ReadTypeInheritanceTest()
{
byte[] structBytes1 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, 0xAA, 0x55, // FieldA
0x55, 0xAA, 0x55, 0xAA, // FieldB
];
var stream1 = new MemoryStream(structBytes1);
var expected1 = new TestStructInheritanceChild1
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA55AA,
FieldB = 0xAA55AA55,
};
var read1 = stream1.ReadType<TestStructInheritanceChild1>();
Assert.NotNull(read1?.Signature);
Assert.Equal(expected1.Signature, read1.Signature);
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
Assert.Equal(expected1.FieldA, read1.FieldA);
Assert.Equal(expected1.FieldB, read1.FieldB);
byte[] structBytes2 =
[
0x41, 0x42, 0x43, 0x44, // Signature
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
0xAA, 0x55, // FieldA
0x55, 0xAA, // FieldB
];
var stream2 = new MemoryStream(structBytes2);
var expected2 = new TestStructInheritanceChild2
{
Signature = [0x41, 0x42, 0x43, 0x44],
IdentifierType = 0xFF00FF00,
FieldA = 0x55AA,
FieldB = 0xAA55,
};
var read2 = stream2.ReadType<TestStructInheritanceChild2>();
Assert.NotNull(read2?.Signature);
Assert.Equal(expected2.Signature, read2.Signature);
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
Assert.Equal(expected2.FieldA, read2.FieldA);
Assert.Equal(expected2.FieldB, read2.FieldB);
}
}
}

View File

@@ -0,0 +1,596 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class StreamWriterExtensionsTests
{
/// <summary>
/// Test pattern from 0x00-0x0F
/// </summary>
private static readonly byte[] _bytes =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Represents the decimal value 0.0123456789
/// </summary>
private static readonly byte[] _decimalBytes =
[
0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00,
];
[Fact]
public void WriteByteValueTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(1).ToArray();
bool write = stream.Write((byte)0x00);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteBytesTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = StreamWriterExtensions.Write(stream, [0x00, 0x01, 0x02, 0x03]);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteBytesBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
stream.WriteBigEndian([0x03, 0x02, 0x01, 0x00]);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSByteTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(1).ToArray();
bool write = stream.Write((sbyte)0x00);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteCharTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(1).ToArray();
bool write = stream.Write('\0');
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteCharEncodingTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = [0x00, 0x00];
stream.Write('\0', Encoding.Unicode);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt16Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.Write((short)0x0100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt16BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.WriteBigEndian((short)0x0001);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt16Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.Write((ushort)0x0100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt16BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.WriteBigEndian((ushort)0x0001);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.Write(BitConverter.Int16BitsToHalf(0x0100));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteHalfBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(2).ToArray();
bool write = stream.WriteBigEndian(BitConverter.Int16BitsToHalf(0x0001));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteInt24Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(3).ToArray();
bool write = stream.WriteAsInt24(0x020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt24BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(3).ToArray();
bool write = stream.WriteAsInt24BigEndian(0x000102);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt24Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(3).ToArray();
bool write = stream.WriteAsUInt24(0x020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt24BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(3).ToArray();
bool write = stream.WriteAsUInt24BigEndian(0x000102);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt32Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.Write(0x03020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt32BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.WriteBigEndian(0x00010203);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt32Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.Write((uint)0x03020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt32BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.WriteBigEndian((uint)0x00010203);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSingleTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.Write(BitConverter.Int32BitsToSingle(0x03020100));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteSingleBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(4).ToArray();
bool write = stream.WriteBigEndian(BitConverter.Int32BitsToSingle(0x00010203));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt48Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(6).ToArray();
bool write = stream.WriteAsInt48(0x050403020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt48BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(6).ToArray();
bool write = stream.WriteAsInt48BigEndian(0x000102030405);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt48Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(6).ToArray();
bool write = stream.WriteAsUInt48(0x050403020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt48BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(6).ToArray();
bool write = stream.WriteAsUInt48BigEndian(0x000102030405);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt64Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.Write(0x0706050403020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt64BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.WriteBigEndian(0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt64Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.Write((ulong)0x0706050403020100);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt64BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.WriteBigEndian((ulong)0x0001020304050607);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDoubleTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.Write(BitConverter.Int64BitsToDouble(0x0706050403020100));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDoubleBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(8).ToArray();
bool write = stream.WriteBigEndian(BitConverter.Int64BitsToDouble(0x0001020304050607));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDecimalTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _decimalBytes.Take(16).ToArray();
bool write = stream.Write(0.0123456789M);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteDecimalBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _decimalBytes.Take(16).Reverse().ToArray();
bool write = stream.WriteBigEndian(0.0123456789M);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteGuidTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.Write(new Guid(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteGuidBigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.WriteBigEndian(new Guid(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.Write((Int128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteInt128BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.WriteBigEndian((Int128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt128Test()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.Write((UInt128)new BigInteger(_bytes));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteUInt128BigEndianTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = _bytes.Take(16).ToArray();
bool write = stream.WriteBigEndian((UInt128)new BigInteger(_bytes.Reverse().ToArray()));
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = stream.WriteNullTerminatedAnsiString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUTF8StringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
byte[] expected = [0x41, 0x42, 0x43, 0x00];
bool write = stream.WriteNullTerminatedUTF8String("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUnicodeStringTest()
{
var stream = new MemoryStream(new byte[8], 0, 8, true, true);
byte[] expected = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00];
bool write = stream.WriteNullTerminatedUnicodeString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteNullTerminatedUTF32StringTest()
{
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
byte[] expected = [0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
bool write = stream.WriteNullTerminatedUTF32String("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WritePrefixedAnsiStringTest()
{
var stream = new MemoryStream(new byte[4], 0, 4, true, true);
byte[] expected = [0x03, 0x41, 0x42, 0x43];
bool write = stream.WritePrefixedAnsiString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WritePrefixedUnicodeStringTest()
{
var stream = new MemoryStream(new byte[8], 0, 8, true, true);
byte[] expected = [0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00];
bool write = stream.WritePrefixedUnicodeString("ABC");
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteTypeTest()
{
// Guid
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
bool actual = stream.WriteType<Guid>(new Guid(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(new byte[2], 0, 2, true, true);
actual = stream.WriteType<Half>(BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], stream.GetBuffer());
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
actual = stream.WriteType<Int128>((Int128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
// UInt128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
actual = stream.WriteType<UInt128>((UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#endif
// Enum
stream = new MemoryStream(new byte[4], 0, 4, true, true);
actual = stream.WriteType<TestEnum>((TestEnum)0x03020100);
Assert.True(actual);
ValidateBytes([.. _bytes.Take(4)], stream.GetBuffer());
}
[Fact]
public void WriteTypeExplicitTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
var obj = new TestStructExplicit
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(12).ToArray();
bool write = stream.WriteType(obj);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
[Fact]
public void WriteTypeSequentialTest()
{
byte[] bytesWithString =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
];
var stream = new MemoryStream(new byte[24], 0, 24, true, true);
var obj = new TestStructSequential
{
FirstValue = TestEnum.RecognizedTestValue,
SecondValue = 0x07060504,
ThirdValue = 0x0908,
FourthValue = 0x0B0A,
FifthValue = "ABC",
};
byte[] expected = bytesWithString.Take(16).ToArray();
bool write = stream.WriteType(obj);
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
/// <summary>
/// Validate that a set of actual bytes matches the expected bytes
/// </summary>
private static void ValidateBytes(byte[] expected, byte[] actual)
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], actual[i]);
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace SabreTools.IO.Test.Extensions
{
internal enum TestEnum : uint
{
None = 0x00000000,
RecognizedTestValue = 0x03020100,
UpperBoundaryValue = 0xFFFFFFFF,
}
}

View File

@@ -0,0 +1,59 @@
using System.Runtime.InteropServices;
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct TestStructArrays
{
/// <summary>
/// 4 entry byte array
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[]? ByteArray;
/// <summary>
/// 4 entry int array
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[]? IntArray;
/// <summary>
/// 4 entry int array
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public TestEnum[]? EnumArray;
/// <summary>
/// 4 entry struct array
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public TestStructPoint[]? StructArray;
/// <summary>
/// Length of <see cref="LPByteArray"/>
/// </summary>
public ushort LPByteArrayLength;
/// <summary>
/// 4 entry byte array whose length is defined by <see cref="LPByteArrayLength"/>
/// </summary>
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)]
public byte[]? LPByteArray;
// /// <summary>
// /// 4 entry nested byte array
// /// </summary>
// /// <remarks>This will likely fail</remarks>
// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
// public byte[][]? NestedByteArray;
}
/// <summary>
/// Struct for nested tests
/// </summary>
internal struct TestStructPoint
{
public ushort X;
public ushort Y;
}
}

View File

@@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Explicit)]
internal struct TestStructExplicit
{
[FieldOffset(0)]
public TestEnum FirstValue;
[FieldOffset(4)]
public int SecondValue;
[FieldOffset(4)]
public ushort ThirdValue;
[FieldOffset(6)]
public short FourthValue;
[FieldOffset(8), MarshalAs(UnmanagedType.LPStr)]
public string? FifthValue;
}
}

View File

@@ -0,0 +1,29 @@
using System.Runtime.InteropServices;
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal class TestStructInheritanceParent
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[]? Signature;
public uint IdentifierType;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal class TestStructInheritanceChild1 : TestStructInheritanceParent
{
public uint FieldA;
public uint FieldB;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal class TestStructInheritanceChild2 : TestStructInheritanceParent
{
public ushort FieldA;
public ushort FieldB;
}
}

View File

@@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Sequential)]
internal struct TestStructSequential
{
public TestEnum FirstValue;
public int SecondValue;
public ushort ThirdValue;
public short FourthValue;
[MarshalAs(UnmanagedType.LPStr)]
public string? FifthValue;
}
}

View File

@@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal struct TestStructStrings
{
/// <summary>
/// ASCII-encoded, byte-length-prefixed string
/// </summary>
[MarshalAs(UnmanagedType.AnsiBStr)]
public string? AnsiBStr;
/// <summary>
/// Unicode-encoded, WORD-length-prefixed string
/// </summary>
[MarshalAs(UnmanagedType.BStr)]
public string? BStr;
/// <summary>
/// Fixed length string
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 3)]
public string? ByValTStr;
/// <summary>
/// ASCII-encoded, null-terminated string
/// </summary>
[MarshalAs(UnmanagedType.LPStr)]
public string? LPStr;
/// <summary>
/// Unicode-encoded, null-terminated string
/// </summary>
[MarshalAs(UnmanagedType.LPWStr)]
public string? LPWStr;
}
}

View File

@@ -0,0 +1,242 @@
using System;
using System.IO;
using System.Text;
using System.Xml;
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class XmlTextWriterExtensionsTests
{
[Fact]
public void WriteRequiredAttributeString_NullInputThrow_Throws()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
Assert.Throws<ArgumentNullException>(()
=> writer.WriteRequiredAttributeString("attr", null, throwOnError: true));
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(52, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteRequiredAttributeString_NullInputNoThrow_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element attr=\"\" />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
writer.WriteRequiredAttributeString("attr", null, throwOnError: false);
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(60, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteRequiredAttributeString_ValidInput_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element attr=\"val\" />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
writer.WriteRequiredAttributeString("attr", "val", throwOnError: false);
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(63, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteRequiredElementString_NullInputThrow_Throws()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
Assert.Throws<ArgumentNullException>(()
=> writer.WriteRequiredElementString("element", null, throwOnError: true));
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(41, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteRequiredElementString_NullInputNoThrow_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element></element>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteRequiredElementString("element", null, throwOnError: false);
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(60, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteRequiredElementString_ValidInput_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element>val</element>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteRequiredElementString("element", "val", throwOnError: false);
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(63, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalAttributeString_NullInput_NoWrite()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
writer.WriteOptionalAttributeString("attr", null);
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(52, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalAttributeString_EmptyInput_NoWrite()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
writer.WriteOptionalAttributeString("attr", string.Empty);
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(52, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalAttributeString_ValidInput_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element attr=\"val\" />";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteStartElement("element");
writer.WriteOptionalAttributeString("attr", "val");
writer.WriteEndElement();
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(63, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalElementString_NullInput_NoWrite()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteOptionalElementString("element", null);
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(41, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalElementString_EmptyInput_NoWrite()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteOptionalElementString("element", string.Empty);
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(41, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
[Fact]
public void WriteOptionalElementString_ValidInput_Writes()
{
string expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><element>val</element>";
var stream = new MemoryStream();
var writer = new XmlTextWriter(stream, Encoding.UTF8);
writer.WriteStartDocument();
writer.WriteOptionalElementString("element", "val");
writer.Flush();
// Length includes UTF-8 BOM
Assert.Equal(63, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
}
}
}

View File

@@ -1,23 +0,0 @@
using Xunit;
namespace SabreTools.IO.Test
{
public class IOExtensionsTests
{
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData("no-extension", null)]
[InlineData("NO-EXTENSION", null)]
[InlineData("no-extension.", null)]
[InlineData("NO-EXTENSION.", null)]
[InlineData("filename.ext", "ext")]
[InlineData("FILENAME.EXT", "ext")]
public void NormalizedExtensionTest(string? path, string? expected)
{
string? actual = path.GetNormalizedExtension();
Assert.Equal(expected, actual);
}
}
}

View File

@@ -0,0 +1,67 @@
using System.IO;
using System.Text;
using Xunit;
namespace SabreTools.IO.Test
{
public class IniFileTests
{
[Fact]
public void EndToEndTest()
{
string expected = "[section1]\nkey1=value1\nkey2=value2\n";
// Build the INI
var iniFile = new IniFile();
iniFile.AddOrUpdate("section1.key1", "value1");
iniFile["section1.key2"] = "value2";
iniFile["section2.key3"] = "REMOVEME";
bool removed = iniFile.Remove("section2.key3");
Assert.True(removed);
Assert.Equal("value1", iniFile["section1.key1"]);
Assert.Equal("value2", iniFile["section1.key2"]);
// Write the INI
var stream = new MemoryStream();
bool write = iniFile.Write(stream);
// Length includes UTF-8 BOM
Assert.True(write);
Assert.Equal(38, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
// Parse the INI
stream.Seek(0, SeekOrigin.Begin);
var secondIni = new IniFile(stream);
Assert.Equal("value1", secondIni["section1.key1"]);
Assert.Equal("value2", secondIni["section1.key2"]);
}
[Fact]
public void RemoveInvalidKeyTest()
{
var iniFile = new IniFile();
bool removed = iniFile.Remove("invalid.key");
Assert.False(removed);
}
[Fact]
public void ReadEmptyStreamTest()
{
var stream = new MemoryStream();
var iniFile = new IniFile(stream);
Assert.Empty(iniFile);
}
[Fact]
public void WriteEmptyIniFileTest()
{
var iniFile = new IniFile();
var stream = new MemoryStream();
bool write = iniFile.Write(stream);
Assert.False(write);
}
}
}

View File

@@ -0,0 +1,38 @@
using SabreTools.IO.Logging;
using Xunit;
namespace SabreTools.IO.Test.Logging
{
public class ConvertersTests
{
[Theory]
[InlineData(null, LogLevel.VERBOSE)]
[InlineData("", LogLevel.VERBOSE)]
[InlineData("INVALID", LogLevel.VERBOSE)]
[InlineData("verbose", LogLevel.VERBOSE)]
[InlineData("VERBOSE", LogLevel.VERBOSE)]
[InlineData("user", LogLevel.USER)]
[InlineData("USER", LogLevel.USER)]
[InlineData("warning", LogLevel.WARNING)]
[InlineData("WARNING", LogLevel.WARNING)]
[InlineData("error", LogLevel.ERROR)]
[InlineData("ERROR", LogLevel.ERROR)]
public void AsLogLevelTest(string? level, LogLevel expected)
{
LogLevel actual = level.AsLogLevel();
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(LogLevel.VERBOSE, "VERBOSE")]
[InlineData(LogLevel.USER, "USER")]
[InlineData(LogLevel.WARNING, "WARNING")]
[InlineData(LogLevel.ERROR, "ERROR")]
[InlineData((LogLevel)99, null)]
public void FromLogLevelTest(LogLevel level, string? expected)
{
string? actual = level.FromLogLevel();
Assert.Equal(expected, actual);
}
}
}

View File

@@ -0,0 +1,40 @@
using SabreTools.IO.Logging;
using Xunit;
namespace SabreTools.IO.Test.Logging
{
public class InternalStopwatchTests
{
[Fact]
public void Stopwatch_NoSubject_StartNoSubject()
{
var stopwatch = new InternalStopwatch();
stopwatch.Start();
stopwatch.Stop();
}
[Fact]
public void Stopwatch_NoSubject_StartSubject()
{
var stopwatch = new InternalStopwatch();
stopwatch.Start("start");
stopwatch.Stop();
}
[Fact]
public void Stopwatch_Subject_StartNoSubject()
{
var stopwatch = new InternalStopwatch("init");
stopwatch.Start();
stopwatch.Stop();
}
[Fact]
public void Stopwatch_Subject_StartSubject()
{
var stopwatch = new InternalStopwatch("init");
stopwatch.Start("start");
stopwatch.Stop();
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using SabreTools.IO.Logging;
using Xunit;
namespace SabreTools.IO.Test.Logging
{
public class LoggerTests
{
[Fact]
public void EndToEnd()
{
Assert.Null(LoggerImpl.Filename);
Assert.False(LoggerImpl.LogToFile);
Assert.Null(LoggerImpl.LogDirectory);
Assert.True(LoggerImpl.AppendPrefix);
Assert.False(LoggerImpl.ThrowOnError);
LoggerImpl.Start();
var logger = new Logger();
logger.Verbose("verbose");
logger.Verbose(new Exception());
logger.Verbose(new Exception(), "verbose");
logger.Verbose(1, 1, "verbose");
logger.User("user");
logger.User(new Exception());
logger.User(new Exception(), "user");
logger.User(1, 1, "user");
logger.Warning("warning");
logger.Warning(new Exception());
logger.Warning(new Exception(), "warning");
logger.Warning(1, 1, "warning");
logger.Error("error");
logger.Error(new Exception());
logger.Error(new Exception(), "error");
logger.Error(1, 1, "error");
LoggerImpl.ThrowOnError = true;
Assert.Throws<Exception>(() => logger.Error(new Exception()));
Assert.True(LoggerImpl.StartTime < DateTime.Now);
Assert.True(LoggerImpl.LoggedWarnings);
Assert.True(LoggerImpl.LoggedErrors);
LoggerImpl.SetFilename("logfile.txt", addDate: true);
LoggerImpl.Close();
}
}
}

View File

@@ -18,6 +18,14 @@ namespace SabreTools.IO.Test
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", true, "SubDir-Filename.ext")]
public void NormalizedFileNameTest(string current, string? parent, bool sanitize, string? expected)
{
// Hack to support Windows paths on Linux for testing only
if (System.IO.Path.DirectorySeparatorChar == '/')
{
current = current.Replace('\\', '/');
parent = parent?.Replace('\\', '/');
expected = expected?.Replace('\\', '/');
}
var path = new ParentablePath(current, parent);
string? actual = path.GetNormalizedFileName(sanitize);
Assert.Equal(expected, actual);
@@ -62,6 +70,15 @@ namespace SabreTools.IO.Test
if (expected?.Contains("%cd%") == true)
expected = expected.Replace("%cd%", Environment.CurrentDirectory.TrimEnd('\\', '/'));
// Hack to support Windows paths on Linux for testing only
if (System.IO.Path.DirectorySeparatorChar == '/')
{
current = current.Replace('\\', '/');
parent = parent?.Replace('\\', '/');
outDir = outDir?.Replace('\\', '/');
expected = expected?.Replace('\\', '/');
}
var path = new ParentablePath(current, parent);
string? actual = path.GetOutputPath(outDir, inplace);
Assert.Equal(expected, actual);

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace SabreTools.IO.Test
{
public class PathToolTests
{
[Fact]
public void GetDirectoriesOnly_NoAppendParent()
{
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData");
string expectedCurrent = Path.Combine(expectedParent, "Subdirectory");
List<string> inputs =
[
string.Empty,
Path.Combine(Environment.CurrentDirectory, "TestData"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Subdir*"),
];
var actual = PathTool.GetDirectoriesOnly(inputs, appendParent: true);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(expectedParent, first.ParentPath);
}
[Fact]
public void GetDirectoriesOnly_AppendParent()
{
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData");
string expectedCurrent = Path.Combine(expectedParent, "Subdirectory");
List<string> inputs =
[
string.Empty,
Path.Combine(Environment.CurrentDirectory, "TestData"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Subdir*"),
];
var actual = PathTool.GetDirectoriesOnly(inputs, appendParent: false);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(string.Empty, first.ParentPath);
}
[Fact]
public void GetFilesOnly_NoAppendParent()
{
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData");
string expectedCurrent = Path.Combine(expectedParent, "ascii.txt");
List<string> inputs =
[
string.Empty,
Path.Combine(Environment.CurrentDirectory, "TestData"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Subdir*"),
Path.Combine(Environment.CurrentDirectory, "TestData", "utf8bom.txt"),
];
var actual = PathTool.GetFilesOnly(inputs, appendParent: true);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(expectedParent, first.ParentPath);
}
[Fact]
public void GetFilesOnly_AppendParent()
{
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData");
string expectedCurrent = Path.Combine(expectedParent, "ascii.txt");
List<string> inputs =
[
string.Empty,
Path.Combine(Environment.CurrentDirectory, "TestData"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Subdir*"),
Path.Combine(Environment.CurrentDirectory, "TestData", "utf8bom.txt"),
];
var actual = PathTool.GetFilesOnly(inputs, appendParent: false);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(string.Empty, first.ParentPath);
}
}
}

View File

@@ -0,0 +1,60 @@
using System.IO;
using System.Text;
using SabreTools.IO.Readers;
using SabreTools.IO.Writers;
using Xunit;
namespace SabreTools.IO.Test.ReadersWriters
{
public class ClrMameProTests
{
[Fact]
public void EndToEndTest()
{
string expected = "header (\n\tstandalone \"value\"\n)\n\n# Comment\n\ngame (\n\titem ( attr \"value\" )\n)";
// Build and write the CMP file
var stream = new MemoryStream();
var writer = new ClrMameProWriter(stream, Encoding.UTF8);
Assert.True(writer.Quotes);
writer.WriteStartElement("header");
writer.WriteRequiredStandalone("standalone", "value");
writer.WriteOptionalStandalone("optstand", null);
writer.WriteFullEndElement();
writer.WriteString("\n\n# Comment\n");
writer.WriteStartElement("game");
writer.WriteStartElement("item");
writer.WriteRequiredAttributeString("attr", "value");
writer.WriteOptionalAttributeString("optional", null);
writer.WriteEndElement();
writer.WriteFullEndElement();
writer.Flush();
writer.Dispose();
// Length includes UTF-8 BOM
Assert.Equal(77, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
// Parse the CMP file
stream.Seek(0, SeekOrigin.Begin);
var reader = new ClrMameProReader(stream, Encoding.UTF8);
Assert.False(reader.DosCenter);
Assert.True(reader.Quotes);
while (!reader.EndOfStream)
{
bool hasNext = reader.ReadNextLine();
Assert.True(hasNext);
Assert.NotNull(reader.CurrentLine);
Assert.True(reader.LineNumber >= 0);
}
reader.Dispose();
}
}
}

View File

@@ -0,0 +1,50 @@
using System.IO;
using System.Text;
using SabreTools.IO.Readers;
using SabreTools.IO.Writers;
using Xunit;
namespace SabreTools.IO.Test.ReadersWriters
{
public class IniTests
{
[Fact]
public void EndToEndTest()
{
string expected = "[section1]\nkey1=value1\nkey2=value2\n\n;comment\n;string\n";
// Build and write the INI
var stream = new MemoryStream();
var writer = new IniWriter(stream, Encoding.UTF8);
writer.WriteSection("section1");
writer.WriteKeyValuePair("key1", "value1");
writer.WriteKeyValuePair("key2", "value2");
writer.WriteLine();
writer.WriteComment("comment");
writer.WriteString(";string\n");
writer.Flush();
writer.Dispose();
// Length includes UTF-8 BOM
Assert.Equal(56, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
// Parse the INI
stream.Seek(0, SeekOrigin.Begin);
var reader = new IniReader(stream, Encoding.UTF8);
while (!reader.EndOfStream)
{
bool hasNext = reader.ReadNextLine();
Assert.True(hasNext);
Assert.NotNull(reader.CurrentLine);
Assert.True(reader.LineNumber >= 0);
}
reader.Dispose();
}
}
}

View File

@@ -0,0 +1,60 @@
using System.IO;
using System.Text;
using SabreTools.IO.Readers;
using SabreTools.IO.Writers;
using Xunit;
namespace SabreTools.IO.Test.ReadersWriters
{
public class SeparatedValueTests
{
[Fact]
public void EndToEndTest()
{
string expected = "\"col1\",\"col2\",\"col3\"\n\"value1\",\"value2\",\"value3\"\n\"value4\",\"value5\",\"value6\"\n";
// Build and write the CSV
var stream = new MemoryStream();
var writer = new SeparatedValueWriter(stream, Encoding.UTF8);
Assert.True(writer.Quotes);
Assert.Equal(',', writer.Separator);
Assert.True(writer.VerifyFieldCount);
writer.WriteHeader(["col1", "col2", "col3"]);
writer.WriteValues(["value1", "value2", "value3"]);
writer.WriteString("\"value4\",\"value5\",\"value6\"\n");
writer.Flush();
writer.Dispose();
// Length includes UTF-8 BOM
Assert.Equal(78, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);
// Parse the CSV
stream.Seek(0, SeekOrigin.Begin);
var reader = new SeparatedValueReader(stream, Encoding.UTF8);
Assert.True(reader.Header);
Assert.True(reader.Quotes);
Assert.Equal(',', reader.Separator);
Assert.True(reader.VerifyFieldCount);
while (!reader.EndOfStream)
{
bool hasNext = reader.ReadNextLine();
Assert.True(hasNext);
Assert.NotNull(reader.CurrentLine);
Assert.True(reader.LineNumber >= 0);
if (reader.LineNumber > 0)
{
Assert.NotNull(reader.GetValue(0));
Assert.NotNull(reader.GetValue("col2"));
}
}
reader.Dispose();
}
}
}

View File

@@ -1,27 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<None Remove="TestData\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
<Content Include="TestData\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,119 @@
using System.IO;
using System.Linq;
using SabreTools.IO.Streams;
using Xunit;
namespace SabreTools.IO.Test.Streams
{
public class ReadOnlyBitStreamTests
{
[Fact]
public void DefaultConstructorTest()
{
var stream = new ReadOnlyBitStream(new MemoryStream());
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
stream = new ReadOnlyBitStream(new MemoryStream(new byte[16], 0, 16, true, true));
Assert.Equal(16, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void ReadSingleBitTest()
{
byte[] data = [0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
byte? bit = stream.ReadBit();
Assert.NotNull(bit);
Assert.Equal((byte)0b00000001, bit);
Assert.Equal(1, stream.Position);
}
[Theory]
[InlineData(4, 0b00000101, 1)]
[InlineData(9, 0b10101010_1, 2)]
public void ReadBitsBETest(int bits, uint expected, int position)
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
uint? actual = stream.ReadBitsBE(bits);
Assert.NotNull(actual);
Assert.Equal(expected, actual);
Assert.Equal(position, stream.Position);
}
[Theory]
[InlineData(4, 0b00001010, 1)]
[InlineData(9, 0b10101010_1, 2)]
public void ReadBitsLETest(int bits, uint expected, int position)
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
uint? actual = stream.ReadBitsLE(bits);
Assert.NotNull(actual);
Assert.Equal(expected, actual);
Assert.Equal(position, stream.Position);
}
[Fact]
public void ReadByteTest()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
byte expected = 0b01010101;
var stream = new ReadOnlyBitStream(new MemoryStream(data));
byte? actual = stream.ReadByte();
Assert.NotNull(actual);
Assert.Equal(expected, actual);
}
[Fact]
public void ReadUInt16Test()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
ushort expected = 0b0101010101010101;
var stream = new ReadOnlyBitStream(new MemoryStream(data));
ushort? actual = stream.ReadUInt16();
Assert.NotNull(actual);
Assert.Equal(expected, actual);
}
[Fact]
public void ReadUInt32Test()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
uint expected = 0b01010101010101010101010101010101;
var stream = new ReadOnlyBitStream(new MemoryStream(data));
uint? actual = stream.ReadUInt32();
Assert.NotNull(actual);
Assert.Equal(expected, actual);
}
[Fact]
public void ReadUInt64Test()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
ulong? actual = stream.ReadUInt64();
Assert.Null(actual);
}
[Fact]
public void ReadBytesTest()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
byte[]? actual = stream.ReadBytes(4);
Assert.NotNull(actual);
Assert.True(data.SequenceEqual(actual));
}
}
}

View File

@@ -0,0 +1,146 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Streams;
using Xunit;
namespace SabreTools.IO.Test.Streams
{
public class ReadOnlyCompositeStreamTests
{
[Fact]
public void DefaultConstructorTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyArrayConstructorTest()
{
Stream[] arr = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyEnumerableConstructorTest()
{
// Empty enumerable constructor
List<Stream> list = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void SingleStreamConstructorTest()
{
var stream = new ReadOnlyCompositeStream(new MemoryStream(new byte[1024]));
Assert.Equal(1024, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledArrayConstructorTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledEnumerableConstructorTest()
{
List<Stream> list = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void AddStreamTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
stream.AddStream(new MemoryStream(new byte[1024]));
Assert.Equal(1024, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyStreamReadTest()
{
var stream = new ReadOnlyCompositeStream();
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(0, read);
}
[Fact]
public void SingleStreamReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamSingleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamMultipleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256]), new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void SingleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
[Fact]
public void MultipleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[128]), new MemoryStream(new byte[128])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
}
}

View File

@@ -0,0 +1 @@
Sample file for subdirectories

View File

@@ -0,0 +1 @@
This doesn't match anything

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

Binary file not shown.

View File

@@ -0,0 +1 @@
+/v

View File

@@ -0,0 +1 @@

View File

@@ -1,174 +0,0 @@
using System;
using System.IO;
namespace SabreTools.IO
{
/// <summary>
/// Big endian reading overloads for BinaryReader
/// </summary>
public static class BinaryReaderExtensions
{
/// <summary>
/// Reads the specified number of bytes from the stream, starting from a specified point in the byte array.
/// </summary>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
/// <param name="count">The number of bytes to read.</param>
/// <returns>The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, or it might be zero if the end of the stream is reached.</returns>
public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count)
{
int retval = reader.Read(buffer, index, count);
Array.Reverse(buffer);
return retval;
}
/// <summary>
/// Reads the specified number of characters from the stream, starting from a specified point in the character array.
/// </summary>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
/// <param name="count">The number of characters to read.</param>
/// <returns>The total number of characters read into the buffer. This might be less than the number of characters requested if that many characters are not currently available, or it might be zero if the end of the stream is reached.</returns>
public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count)
{
int retval = reader.Read(buffer, index, count);
Array.Reverse(buffer);
return retval;
}
/// <summary>
/// Reads the specified number of bytes from the current stream into a byte array and advances the current position by that number of bytes.
/// </summary>
/// <param name="count">The number of bytes to read. This value must be 0 or a non-negative number or an exception will occur.</param>
/// <returns>A byte array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count)
{
byte[] retval = reader.ReadBytes(count);
Array.Reverse(retval);
return retval;
}
/// <summary>
/// Reads the specified number of characters from the current stream, returns the data in a character array, and advances the current position in accordance with the Encoding used and the specific character being read from the stream.
/// </summary>
/// <param name="count">The number of characters to read. This value must be 0 or a non-negative number or an exception will occur.</param>
/// <returns>A character array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
public static char[] ReadCharsBigEndian(this BinaryReader reader, int count)
{
char[] retval = reader.ReadChars(count);
Array.Reverse(retval);
return retval;
}
/// <summary>
/// Reads a decimal value from the current stream and advances the current position of the stream by sixteen bytes.
/// </summary>
/// <returns>A decimal value read from the current stream.</returns>
public static decimal ReadDecimalBigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(16);
Array.Reverse(retval);
int i1 = BitConverter.ToInt32(retval, 0);
int i2 = BitConverter.ToInt32(retval, 4);
int i3 = BitConverter.ToInt32(retval, 8);
int i4 = BitConverter.ToInt32(retval, 12);
return new decimal([i1, i2, i3, i4]);
}
/// <summary>
/// eads an 8-byte floating point value from the current stream and advances the current position of the stream by eight bytes.
/// </summary>
/// <returns>An 8-byte floating point value read from the current stream.</returns>
public static double ReadDoubleBigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(8);
Array.Reverse(retval);
return BitConverter.ToDouble(retval, 0);
}
/// <summary>
/// Reads a 2-byte signed integer from the current stream and advances the current position of the stream by two bytes.
/// </summary>
/// <returns>A 2-byte signed integer read from the current stream.</returns>
public static short ReadInt16BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(2);
Array.Reverse(retval);
return BitConverter.ToInt16(retval, 0);
}
/// <summary>
/// Reads a 4-byte signed integer from the current stream and advances the current position of the stream by four bytes.
/// </summary>
/// <returns>A 4-byte signed integer read from the current stream.</returns>
public static int ReadInt32BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(4);
Array.Reverse(retval);
return BitConverter.ToInt32(retval, 0);
}
/// <summary>
/// Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes.
/// </summary>
/// <returns>An 8-byte signed integer read from the current stream.</returns>
public static long ReadInt64BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(8);
Array.Reverse(retval);
return BitConverter.ToInt64(retval, 0);
}
/// <summary>
/// Reads a 4-byte floating point value from the current stream and advances the current position of the stream by four bytes.
/// </summary>
/// <returns>A 4-byte floating point value read from the current stream.</returns>
public static float ReadSingleBigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(4);
Array.Reverse(retval);
return BitConverter.ToSingle(retval, 0);
}
/// <summary>
/// Reads a 2-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by two bytes.
///
/// This API is not CLS-compliant.
/// </summary>
/// <returns>A 2-byte unsigned integer read from this stream.</returns>
public static ushort ReadUInt16BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(2);
Array.Reverse(retval);
return BitConverter.ToUInt16(retval, 0);
}
/// <summary>
/// Reads a 4-byte unsigned integer from the current stream and advances the position of the stream by four bytes.
///
/// This API is not CLS-compliant.
/// </summary>
/// <returns>A 4-byte unsigned integer read from this stream.</returns>
public static uint ReadUInt32BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(4);
Array.Reverse(retval);
return BitConverter.ToUInt32(retval, 0);
}
/// <summary>
/// Reads an 8-byte unsigned integer from the current stream and advances the position of the stream by eight bytes.
///
/// This API is not CLS-compliant.
/// </summary>
/// <returns>An 8-byte unsigned integer read from this stream.</returns>
public static ulong ReadUInt64BigEndian(this BinaryReader reader)
{
byte[] retval = reader.ReadBytes(8);
Array.Reverse(retval);
return BitConverter.ToUInt64(retval, 0);
}
}
}

View File

@@ -1,283 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SabreTools.IO
{
/// <summary>
/// Extensions for byte arrays
/// </summary>
/// <remarks>TODO: Add U/Int24 and U/Int48 methods</remarks>
public static class ByteArrayExtensions
{
/// <summary>
/// Read a UInt8 and increment the pointer to an array
/// </summary>
public static byte ReadByte(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 1);
if (buffer == null)
return default;
return buffer[0];
}
/// <summary>
/// Read a UInt8[] and increment the pointer to an array
/// </summary>
public static byte[]? ReadBytes(this byte[]? content, ref int offset, int count)
{
// If the byte array is invalid, don't do anything
if (content == null)
return null;
// If there's an invalid byte count, don't do anything
if (count <= 0 || offset >= content.Length)
return null;
// Allocate enough space for the data requested
byte[] buffer = new byte[count];
// If we have less data left than requested, only read until the end
if (offset + count >= content.Length)
count = content.Length - offset;
// If we have a non-zero count, copy the data into the array
if (count > 0)
Array.Copy(content, offset, buffer, 0, Math.Min(count, content.Length - offset));
// Increment the offset and return
offset += count;
return buffer;
}
/// <summary>
/// Read a Int8 and increment the pointer to an array
/// </summary>
public static sbyte ReadSByte(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 1);
if (buffer == null)
return default;
return (sbyte)buffer[0];
}
/// <summary>
/// Read a Char and increment the pointer to an array
/// </summary>
public static char ReadChar(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 1);
if (buffer == null)
return default;
return (char)buffer[0];
}
/// <summary>
/// Read a Int16 and increment the pointer to an array
/// </summary>
public static short ReadInt16(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 2);
if (buffer == null)
return default;
return BitConverter.ToInt16(buffer, 0);
}
/// <summary>
/// Read a Int16 in big-endian format and increment the pointer to an array
/// </summary>
public static short ReadInt16BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 2);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToInt16(buffer, 0);
}
/// <summary>
/// Read a UInt16 and increment the pointer to an array
/// </summary>
public static ushort ReadUInt16(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 2);
if (buffer == null)
return default;
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a UInt16 in big-endian format and increment the pointer to an array
/// </summary>
public static ushort ReadUInt16BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 2);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a Int32 and increment the pointer to an array
/// </summary>
public static int ReadInt32(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 4);
if (buffer == null)
return default;
return BitConverter.ToInt32(buffer, 0);
}
/// <summary>
/// Read a Int32 in big-endian format and increment the pointer to an array
/// </summary>
public static int ReadInt32BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 4);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToInt32(buffer, 0);
}
/// <summary>
/// Read a UInt32 and increment the pointer to an array
/// </summary>
public static uint ReadUInt32(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 4);
if (buffer == null)
return default;
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a UInt32 in big-endian format and increment the pointer to an array
/// </summary>
public static uint ReadUInt32BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 4);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a Int64 and increment the pointer to an array
/// </summary>
public static long ReadInt64(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 8);
if (buffer == null)
return default;
return BitConverter.ToInt64(buffer, 0);
}
/// <summary>
/// Read a Int64 in big-endian format and increment the pointer to an array
/// </summary>
public static long ReadInt64BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 8);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToInt64(buffer, 0);
}
/// <summary>
/// Read a UInt64 and increment the pointer to an array
/// </summary>
public static ulong ReadUInt64(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 8);
if (buffer == null)
return default;
return BitConverter.ToUInt64(buffer, 0);
}
/// <summary>
/// Read a UInt64 in big-endian format and increment the pointer to an array
/// </summary>
public static ulong ReadUInt64BigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 8);
if (buffer == null)
return default;
Array.Reverse(buffer);
return BitConverter.ToUInt64(buffer, 0);
}
/// <summary>
/// Read a Guid and increment the pointer to an array
/// </summary>
public static Guid ReadGuid(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 16);
if (buffer == null)
return default;
return new Guid(buffer);
}
/// <summary>
/// Read a Guid in big-endian format and increment the pointer to an array
/// </summary>
public static Guid ReadGuidBigEndian(this byte[] content, ref int offset)
{
byte[]? buffer = content.ReadBytes(ref offset, 16);
if (buffer == null)
return default;
Array.Reverse(buffer);
return new Guid(buffer);
}
/// <summary>
/// Read a null-terminated string from the stream
/// </summary>
public static string? ReadString(this byte[] content, ref int offset) => content.ReadString(ref offset, Encoding.Default);
/// <summary>
/// Read a null-terminated string from the stream
/// </summary>
public static string? ReadString(this byte[] content, ref int offset, Encoding encoding)
{
if (offset >= content.Length)
return null;
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
int charWidth = nullTerminator.Length;
var keyChars = new List<char>();
while (offset < content.Length)
{
char c = encoding.GetChars(content, offset, charWidth)[0];
keyChars.Add(c);
offset += charWidth;
if (c == '\0')
break;
}
return new string([.. keyChars]).TrimEnd('\0');
}
}
}

View File

@@ -0,0 +1,9 @@
#if NET20
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
internal sealed class ExtensionAttribute : Attribute {}
}
#endif

View File

@@ -0,0 +1,905 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for BinaryReader
/// </summary>
public static class BinaryReaderExtensions
{
/// <inheritdoc cref="BinaryReader.Read(byte[], int, int)"/>
/// <remarks>Reads in big-endian format</remarks>
public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count)
{
int retval = reader.Read(buffer, index, count);
Array.Reverse(buffer);
return retval;
}
/// <inheritdoc cref="BinaryReader.Read(char[], int, int)"/>
/// <remarks>Reads in big-endian format</remarks>
public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count)
{
int retval = reader.Read(buffer, index, count);
Array.Reverse(buffer);
return retval;
}
/// <inheritdoc cref="BinaryReader.ReadBytes(int)"/>
/// <remarks>Reads in big-endian format</remarks>
public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count)
{
byte[] buffer = reader.ReadBytes(count);
Array.Reverse(buffer);
return buffer;
}
/// <inheritdoc cref="BinaryReader.ReadChars(int)"/>
/// <remarks>Reads in big-endian format</remarks>
public static char[] ReadCharsBigEndian(this BinaryReader reader, int count)
{
char[] buffer = reader.ReadChars(count);
Array.Reverse(buffer);
return buffer;
}
/// <inheritdoc cref="BinaryReader.ReadInt16"/>
/// <remarks>Reads in big-endian format</remarks>
public static short ReadInt16BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(2);
return (short)(buffer[1]
| (buffer[0] << 8));
}
/// <inheritdoc cref="BinaryReader.ReadInt16"/>
/// <remarks>Reads in little-endian format</remarks>
public static short ReadInt16LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(2);
return (short)(buffer[0]
| (buffer[1] << 8));
}
/// <inheritdoc cref="BinaryReader.ReadUInt16"/>
/// <remarks>Reads in big-endian format</remarks>
public static ushort ReadUInt16BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(2);
return (ushort)(buffer[1]
| (buffer[0] << 8));
}
/// <inheritdoc cref="BinaryReader.ReadUInt16"/>
/// <remarks>Reads in little-endian format</remarks>
public static ushort ReadUInt16LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(2);
return (ushort)(buffer[0]
| (buffer[1] << 8));
}
/// <summary>
/// Read a WORD (2-byte) from the base stream
/// </summary>
public static ushort ReadWORD(this BinaryReader reader)
=> reader.ReadUInt16();
/// <summary>
/// Read a WORD (2-byte) from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ushort ReadWORDBigEndian(this BinaryReader reader)
=> reader.ReadUInt16BigEndian();
/// <summary>
/// Read a WORD (2-byte) from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static ushort ReadWORDLittleEndian(this BinaryReader reader)
=> reader.ReadUInt16LittleEndian();
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <inheritdoc cref="BinaryReader.ReadHalf"/>
/// <remarks>Reads in big-endian format</remarks>
public static Half ReadHalfBigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(2);
Array.Reverse(buffer);
return BitConverter.ToHalf(buffer, 0);
}
#endif
/// <summary>
/// Read an Int24 encoded as an Int32 from the base stream
/// </summary>
/// <remarks>Reads in machine native format</remarks>
public static int ReadInt24(this BinaryReader reader)
{
if (BitConverter.IsLittleEndian)
return reader.ReadInt24LittleEndian();
else
return reader.ReadInt24BigEndian();
}
/// <summary>
/// Read an Int24 encoded as an Int32 from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static int ReadInt24BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(3);
return (int)(buffer[2]
| (buffer[1] << 8)
| (buffer[0] << 16));
}
/// <summary>
/// Read an Int24 encoded as an Int32 from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static int ReadInt24LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(3);
return (int)(buffer[0]
| (buffer[1] << 8)
| (buffer[2] << 16));
}
/// <summary>
/// Read a UInt24 encoded as a UInt32 from the base stream
/// </summary>
/// <remarks>Reads in machine native format</remarks>
public static uint ReadUInt24(this BinaryReader reader)
{
if (BitConverter.IsLittleEndian)
return reader.ReadUInt24LittleEndian();
else
return reader.ReadUInt24BigEndian();
}
/// <summary>
/// Read a UInt24 encoded as a UInt32 from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadUInt24BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(3);
return (uint)(buffer[2]
| (buffer[1] << 8)
| (buffer[0] << 16));
}
/// <summary>
/// Read a UInt24 encoded as a UInt32 from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static uint ReadUInt24LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(3);
return (uint)(buffer[0]
| (buffer[1] << 8)
| (buffer[2] << 16));
}
/// <inheritdoc cref="BinaryReader.ReadInt32"/>
/// <remarks>Reads in big-endian format</remarks>
public static int ReadInt32BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(4);
return (int)(buffer[3]
| (buffer[2] << 8)
| (buffer[1] << 16)
| (buffer[0] << 24));
}
/// <inheritdoc cref="BinaryReader.ReadInt32"/>
/// <remarks>Reads in little-endian format</remarks>
public static int ReadInt32LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(4);
return (int)(buffer[0]
| (buffer[1] << 8)
| (buffer[2] << 16)
| (buffer[3] << 24));
}
/// <inheritdoc cref="BinaryReader.ReadUInt32"/>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadUInt32BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(4);
return (uint)(buffer[3]
| (buffer[2] << 8)
| (buffer[1] << 16)
| (buffer[0] << 24));
}
/// <inheritdoc cref="BinaryReader.ReadUInt32"/>
/// <remarks>Reads in little-endian format</remarks>
public static uint ReadUInt32LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(4);
return (uint)(buffer[0]
| (buffer[1] << 8)
| (buffer[2] << 16)
| (buffer[3] << 24));
}
/// <summary>
/// Read a DWORD (4-byte) from the base stream
/// </summary>
public static uint ReadDWORD(this BinaryReader reader)
=> reader.ReadUInt32();
/// <summary>
/// Read a DWORD (4-byte) from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadDWORDBigEndian(this BinaryReader reader)
=> reader.ReadUInt32BigEndian();
/// <summary>
/// Read a DWORD (4-byte) from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static uint ReadDWORDLittleEndian(this BinaryReader reader)
=> reader.ReadUInt32LittleEndian();
/// <inheritdoc cref="BinaryReader.ReadSingle"/>
/// <remarks>Reads in big-endian format</remarks>
public static float ReadSingleBigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(4);
Array.Reverse(buffer);
return BitConverter.ToSingle(buffer, 0);
}
/// <summary>
/// Read an Int48 encoded as an Int64 from the base stream
/// </summary>
/// <remarks>Reads in machine native format</remarks>
public static long ReadInt48(this BinaryReader reader)
{
if (BitConverter.IsLittleEndian)
return reader.ReadInt48LittleEndian();
else
return reader.ReadInt48BigEndian();
}
/// <summary>
/// Read an Int48 encoded as an Int64 from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static long ReadInt48BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(6);
return ((long)buffer[5] << 0)
| ((long)buffer[4] << 8)
| ((long)buffer[3] << 16)
| ((long)buffer[2] << 24)
| ((long)buffer[1] << 32)
| ((long)buffer[0] << 40);
}
/// <summary>
/// Read an Int48 encoded as an Int64 from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static long ReadInt48LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(6);
return ((long)buffer[0] << 0)
| ((long)buffer[1] << 8)
| ((long)buffer[2] << 16)
| ((long)buffer[3] << 24)
| ((long)buffer[4] << 32)
| ((long)buffer[5] << 40);
}
/// <summary>
/// Read a UInt48 encoded as a UInt64 from the base stream
/// </summary>
/// <remarks>Reads in machine native format</remarks>
public static ulong ReadUInt48(this BinaryReader reader)
{
if (BitConverter.IsLittleEndian)
return reader.ReadUInt48LittleEndian();
else
return reader.ReadUInt48BigEndian();
}
/// <summary>
/// Read a UInt48 encoded as a UInt64 from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ulong ReadUInt48BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(6);
return ((ulong)buffer[5] << 0)
| ((ulong)buffer[4] << 8)
| ((ulong)buffer[3] << 16)
| ((ulong)buffer[2] << 24)
| ((ulong)buffer[1] << 32)
| ((ulong)buffer[0] << 40);
}
/// <summary>
/// Read an UInt48 encoded as an UInt64 from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static ulong ReadUInt48LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(6);
return ((ulong)buffer[0] << 0)
| ((ulong)buffer[1] << 8)
| ((ulong)buffer[2] << 16)
| ((ulong)buffer[3] << 24)
| ((ulong)buffer[4] << 32)
| ((ulong)buffer[5] << 40);
}
/// <inheritdoc cref="BinaryReader.ReadInt64"/>
/// <remarks>Reads in big-endian format</remarks>
public static long ReadInt64BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(8);
return ((long)buffer[7] << 0)
| ((long)buffer[6] << 8)
| ((long)buffer[5] << 16)
| ((long)buffer[4] << 24)
| ((long)buffer[3] << 32)
| ((long)buffer[2] << 40)
| ((long)buffer[1] << 48)
| ((long)buffer[0] << 56);
}
/// <inheritdoc cref="BinaryReader.ReadInt64"/>
/// <remarks>Reads in little-endian format</remarks>
public static long ReadInt64LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(8);
return ((long)buffer[0] << 0)
| ((long)buffer[1] << 8)
| ((long)buffer[2] << 16)
| ((long)buffer[3] << 24)
| ((long)buffer[4] << 32)
| ((long)buffer[5] << 40)
| ((long)buffer[6] << 48)
| ((long)buffer[7] << 56);
}
/// <inheritdoc cref="BinaryReader.ReadUInt64"/>
/// <remarks>Reads in big-endian format</remarks>
public static ulong ReadUInt64BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(8);
return ((ulong)buffer[7] << 0)
| ((ulong)buffer[6] << 8)
| ((ulong)buffer[5] << 16)
| ((ulong)buffer[4] << 24)
| ((ulong)buffer[3] << 32)
| ((ulong)buffer[2] << 40)
| ((ulong)buffer[1] << 48)
| ((ulong)buffer[0] << 56);
}
/// <inheritdoc cref="BinaryReader.ReadUInt64"/>
/// <remarks>Reads in little-endian format</remarks>
public static ulong ReadUInt64LittleEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(8);
return ((ulong)buffer[0] << 0)
| ((ulong)buffer[1] << 8)
| ((ulong)buffer[2] << 16)
| ((ulong)buffer[3] << 24)
| ((ulong)buffer[4] << 32)
| ((ulong)buffer[5] << 40)
| ((ulong)buffer[6] << 48)
| ((ulong)buffer[7] << 56);
}
/// <summary>
/// Read a QWORD (8-byte) from the base stream
/// </summary>
/// <remarks>Reads in machine native format</remarks>
public static ulong ReadQWORD(this BinaryReader reader)
=> reader.ReadUInt64();
/// <summary>
/// Read a QWORD (8-byte) from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ulong ReadQWORDBigEndian(this BinaryReader reader)
=> reader.ReadUInt64BigEndian();
/// <summary>
/// Read a QWORD (8-byte) from the base stream
/// </summary>
/// <remarks>Reads in little-endian format</remarks>
public static ulong ReadQWORDLittleEndian(this BinaryReader reader)
=> reader.ReadUInt64LittleEndian();
/// <inheritdoc cref="BinaryReader.ReadDouble"/>
/// <remarks>Reads in big-endian format</remarks>
public static double ReadDoubleBigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(8);
Array.Reverse(buffer);
return BitConverter.ToDouble(buffer, 0);
}
/// <inheritdoc cref="BinaryReader.ReadDecimal"/>
/// <remarks>Reads in big-endian format</remarks>
public static decimal ReadDecimalBigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
Array.Reverse(buffer);
int lo = BitConverter.ToInt32(buffer, 0);
int mid = BitConverter.ToInt32(buffer, 4);
int hi = BitConverter.ToInt32(buffer, 8);
int flags = BitConverter.ToInt32(buffer, 12);
return new decimal([lo, mid, hi, flags]);
}
/// <summary>
/// Read a Guid from the underlying stream
/// </summary>
public static Guid ReadGuid(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
return new Guid(buffer);
}
/// <summary>
/// Read a Guid from the underlying stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static Guid ReadGuidBigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
Array.Reverse(buffer);
return new Guid(buffer);
}
// TODO: Determine if the reverse reads are doing what are expected
#if NET7_0_OR_GREATER
/// <summary>
/// Read an Int128 from the underlying stream
/// </summary>
public static Int128 ReadInt128(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
return (Int128)new BigInteger(buffer);
}
/// <summary>
/// Read an Int128 from the underlying stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static Int128 ReadInt128BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
Array.Reverse(buffer);
return (Int128)new BigInteger(buffer);
}
/// <summary>
/// Read a UInt128 from the underlying stream
/// </summary>
public static UInt128 ReadUInt128(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
return (UInt128)new BigInteger(buffer);
}
/// <summary>
/// Read a UInt128 from the underlying stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static UInt128 ReadUInt128BigEndian(this BinaryReader reader)
{
byte[] buffer = reader.ReadBytes(16);
Array.Reverse(buffer);
return (UInt128)new BigInteger(buffer);
}
#endif
/// <summary>
/// Read a null-terminated string from the underlying stream
/// </summary>
public static string? ReadNullTerminatedString(this BinaryReader reader, Encoding encoding)
{
// Short-circuit to explicit implementations
if (encoding.Equals(Encoding.ASCII))
return reader.ReadNullTerminatedAnsiString();
else if (encoding.Equals(Encoding.UTF8))
return reader.ReadNullTerminatedUTF8String();
else if (encoding.Equals(Encoding.Unicode))
return reader.ReadNullTerminatedUnicodeString();
else if (encoding.Equals(Encoding.UTF32))
return reader.ReadNullTerminatedUTF32String();
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
List<byte> buffer = [];
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
byte ch = reader.ReadByte();
if (ch == '\0')
break;
buffer.Add(ch);
}
return encoding.GetString([.. buffer]);
}
/// <summary>
/// Read a null-terminated ASCII string from the underlying stream
/// </summary>
public static string? ReadNullTerminatedAnsiString(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
byte[] buffer = ReadUntilNull1Byte(reader);
return Encoding.ASCII.GetString(buffer);
}
/// <summary>
/// Read a null-terminated UTF-8 string from the underlying stream
/// </summary>
public static string? ReadNullTerminatedUTF8String(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
byte[] buffer = ReadUntilNull1Byte(reader);
return Encoding.ASCII.GetString(buffer);
}
/// <summary>
/// Read a null-terminated UTF-16 (Unicode) string from the underlying stream
/// </summary>
public static string? ReadNullTerminatedUnicodeString(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
byte[] buffer = ReadUntilNull2Byte(reader);
return Encoding.Unicode.GetString(buffer);
}
/// <summary>
/// Read a null-terminated UTF-32 string from the underlying stream
/// </summary>
public static string? ReadNullTerminatedUTF32String(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
byte[] buffer = ReadUntilNull4Byte(reader);
return Encoding.UTF32.GetString(buffer);
}
/// <summary>
/// Read a byte-prefixed ASCII string from the underlying stream
/// </summary>
public static string? ReadPrefixedAnsiString(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
byte size = reader.ReadByte();
if (reader.BaseStream.Position + size >= reader.BaseStream.Length)
return null;
byte[] buffer = reader.ReadBytes(size);
return Encoding.ASCII.GetString(buffer);
}
/// <summary>
/// Read a ushort-prefixed Unicode string from the underlying stream
/// </summary>
public static string? ReadPrefixedUnicodeString(this BinaryReader reader)
{
if (reader.BaseStream.Position >= reader.BaseStream.Length)
return null;
ushort size = reader.ReadUInt16();
if (reader.BaseStream.Position + (size * 2) >= reader.BaseStream.Length)
return null;
byte[] buffer = reader.ReadBytes(size * 2);
return Encoding.Unicode.GetString(buffer);
}
/// <summary>
/// Read a <typeparamref name="T"/> from the underlying stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are read by value, not by reference
/// - Complex objects are read by value, not by reference
/// - Enumeration values are read by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are deserialized BEFORE fields in the child
/// </remarks>
public static T? ReadType<T>(this BinaryReader reader)
=> (T?)reader.ReadType(typeof(T));
/// <summary>
/// Read a <paramref name="type"/> from the underlying stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are read by value, not by reference
/// - Complex objects are read by value, not by reference
/// - Enumeration values are read by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are deserialized BEFORE fields in the child
/// </remarks>
public static object? ReadType(this BinaryReader reader, Type type)
{
// Handle special struct cases
if (type == typeof(Guid))
return reader.ReadGuid();
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return reader.ReadHalf();
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return reader.ReadInt128();
else if (type == typeof(UInt128))
return reader.ReadUInt128();
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return ReadComplexType(reader, type);
else if (type.IsValueType && type.IsEnum)
return ReadNormalType(reader, Enum.GetUnderlyingType(type));
else
return ReadNormalType(reader, type);
}
/// <summary>
/// Read a <paramref name="type"/> from the underlying stream
/// </summary>
private static object? ReadNormalType(BinaryReader reader, Type type)
{
try
{
int typeSize = Marshal.SizeOf(type);
byte[] buffer = reader.ReadBytes(typeSize); ;
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
var data = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), type);
handle.Free();
return data;
}
catch
{
return null;
}
}
/// <summary>
/// Read a <paramref name="type"/> from the underlying stream
/// </summary>
private static object? ReadComplexType(BinaryReader reader, Type type)
{
try
{
// Try to create an instance of the type
var instance = Activator.CreateInstance(type);
if (instance == null)
return null;
// Get the layout information
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
// Cache the current offset
long currentOffset = reader.BaseStream.Position;
// Generate the fields by parent first
var fields = MarshalHelpers.GetFields(type);
// Loop through the fields and set them
foreach (var fi in fields)
{
// If we have an explicit layout, move accordingly
if (layoutKind == LayoutKind.Explicit)
{
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
reader.BaseStream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
}
SetField(reader, encoding, fields, instance, fi);
}
return instance;
}
catch
{
return null;
}
}
/// <summary>
/// Set a single field on an object
/// </summary>
private static void SetField(BinaryReader reader, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
{
if (fi.FieldType.IsAssignableFrom(typeof(string)))
{
var value = ReadStringType(reader, encoding, fi);
fi.SetValue(instance, value);
}
else if (fi.FieldType.IsArray)
{
var value = ReadArrayType(reader, fields, instance, fi);
if (value.GetType() == fi.FieldType)
fi.SetValue(instance, value);
else
fi.SetValue(instance, Convert.ChangeType(value, fi.FieldType));
}
else
{
var value = reader.ReadType(fi.FieldType);
fi.SetValue(instance, value);
}
}
/// <summary>
/// Read an array type field for an object
/// </summary>
private static Array ReadArrayType(BinaryReader reader, FieldInfo[] fields, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
if (marshalAsAttr == null)
return new object[0];
// Get the number of elements expected
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
if (elementCount < 0)
return new object[0];
// Get the item type for the array
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
// Loop through and build the array
Array arr = Array.CreateInstance(elementType, elementCount);
for (int i = 0; i < elementCount; i++)
{
var value = ReadType(reader, elementType);
if (value != null && elementType.IsEnum)
arr.SetValue(Enum.ToObject(elementType, value), i);
else
arr.SetValue(value, i);
}
// Return the built array
return arr;
}
/// <summary>
/// Read a string type field for an object
/// </summary>
private static string? ReadStringType(BinaryReader reader, Encoding encoding, FieldInfo? fi)
{
// If the FieldInfo is null
if (fi == null)
return null;
// Get all MarshalAs attributes for the field, if possible
var attributes = fi.GetCustomAttributes(typeof(MarshalAsAttribute), true);
if (attributes.Length == 0)
return null;
// Use the first found attribute
var marshalAsAttr = attributes[0] as MarshalAsAttribute;
switch (marshalAsAttr?.Value)
{
case UnmanagedType.AnsiBStr:
return reader.ReadPrefixedAnsiString();
case UnmanagedType.BStr:
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
return reader.ReadPrefixedUnicodeString();
case UnmanagedType.ByValTStr:
int byvalLength = marshalAsAttr!.SizeConst;
byte[] byvalBytes = reader.ReadBytes(byvalLength);
return encoding.GetString(byvalBytes);
case UnmanagedType.LPStr:
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
case null:
return reader.ReadNullTerminatedAnsiString();
#if NET472_OR_GREATER || NETCOREAPP
case UnmanagedType.LPUTF8Str:
return reader.ReadNullTerminatedUTF8String();
#endif
case UnmanagedType.LPWStr:
return reader.ReadNullTerminatedUnicodeString();
// No other string types are recognized
default:
return null;
}
}
/// <summary>
/// Read bytes until a 1-byte null terminator is found
/// </summary>
private static byte[] ReadUntilNull1Byte(BinaryReader reader)
{
var bytes = new List<byte>();
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
byte next = reader.ReadByte();
if (next == 0x00)
break;
bytes.Add(next);
}
return [.. bytes];
}
/// <summary>
/// Read bytes until a 2-byte null terminator is found
/// </summary>
private static byte[] ReadUntilNull2Byte(BinaryReader reader)
{
var bytes = new List<byte>();
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
ushort next = reader.ReadUInt16();
if (next == 0x0000)
break;
bytes.AddRange(BitConverter.GetBytes(next));
}
return [.. bytes];
}
/// <summary>
/// Read bytes until a 4-byte null terminator is found
/// </summary>
private static byte[] ReadUntilNull4Byte(BinaryReader reader)
{
var bytes = new List<byte>();
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
uint next = reader.ReadUInt32();
if (next == 0x00000000)
break;
bytes.AddRange(BitConverter.GetBytes(next));
}
return [.. bytes];
}
}
}

View File

@@ -0,0 +1,646 @@
using System;
using System.IO;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for BinaryWriter
/// </summary>
/// TODO: Handle proper negative values for Int24 and Int48
public static class BinaryWriterExtensions
{
/// <inheritdoc cref="BinaryWriter.Write(byte[])"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, byte[] value)
{
Array.Reverse(value);
return WriteFromBuffer(writer, value);
}
/// <inheritdoc cref="BinaryWriter.Write(char)"/>
public static bool Write(this BinaryWriter writer, char value, Encoding encoding)
{
byte[] buffer = encoding.GetBytes($"{value}");
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(short)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, short value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(ushort)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, ushort value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <inheritdoc cref="BinaryWriter.Write(Half)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, Half value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
#endif
/// <summary>
/// Write an Int32 as an Int24 to the underlying stream
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24(this BinaryWriter writer, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write an Int32 as an Int24 to the underlying stream
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24BigEndian(this BinaryWriter writer, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24 to the underlying stream
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24(this BinaryWriter writer, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24 to the underlying stream
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24BigEndian(this BinaryWriter writer, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(writer, reduced);
}
/// <inheritdoc cref="BinaryWriter.Write(int)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(uint)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(float)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, float value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write an Int64 as an Int48 to the underlying stream
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48(this BinaryWriter writer, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write an Int64 as an Int48 to the underlying stream
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48BigEndian(this BinaryWriter writer, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48 to the underlying stream
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48(this BinaryWriter writer, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(writer, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48 to the underlying stream
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48BigEndian(this BinaryWriter writer, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(writer, reduced);
}
/// <inheritdoc cref="BinaryWriter.Write(long)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(ulong)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(double)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, double value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <inheritdoc cref="BinaryWriter.Write(decimal)"/>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, decimal value)
{
int[] bits = decimal.GetBits(value);
byte[] lo = BitConverter.GetBytes(bits[0]);
byte[] mid = BitConverter.GetBytes(bits[1]);
byte[] hi = BitConverter.GetBytes(bits[2]);
byte[] flags = BitConverter.GetBytes(bits[3]);
byte[] buffer = new byte[16];
Array.Copy(lo, 0, buffer, 0, 4);
Array.Copy(mid, 0, buffer, 4, 4);
Array.Copy(hi, 0, buffer, 8, 4);
Array.Copy(flags, 0, buffer, 12, 4);
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write a Guid
/// </summary>
public static bool Write(this BinaryWriter writer, Guid value)
{
byte[] buffer = value.ToByteArray();
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write a Guid
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, Guid value)
{
byte[] buffer = value.ToByteArray();
Array.Reverse(buffer);
return WriteFromBuffer(writer, buffer);
}
#if NET7_0_OR_GREATER
/// <summary>
/// Write an Int128
/// </summary>
public static bool Write(this BinaryWriter writer, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(writer, padded);
}
/// <summary>
/// Write an Int128
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(writer, padded);
}
/// <summary>
/// Write a UInt128
/// </summary>
public static bool Write(this BinaryWriter writer, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(writer, padded);
}
/// <summary>
/// Write a UInt128
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this BinaryWriter writer, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(writer, padded);
}
#endif
/// <summary>
/// Write a null-terminated string to the underlying stream
/// </summary>
public static bool WriteNullTerminatedString(this BinaryWriter writer, string? value, Encoding encoding)
{
// If the value is null
if (value == null)
return false;
// Add the null terminator and write
value += "\0";
byte[] buffer = encoding.GetBytes(value);
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write a null-terminated ASCII string to the underlying stream
/// </summary>
public static bool WriteNullTerminatedAnsiString(this BinaryWriter writer, string? value)
=> writer.WriteNullTerminatedString(value, Encoding.ASCII);
/// <summary>
/// Write a null-terminated UTF-8 string to the underlying stream
/// </summary>
public static bool WriteNullTerminatedUTF8String(this BinaryWriter writer, string? value)
=> writer.WriteNullTerminatedString(value, Encoding.UTF8);
/// <summary>
/// Write a null-terminated UTF-16 (Unicode) string to the underlying stream
/// </summary>
public static bool WriteNullTerminatedUnicodeString(this BinaryWriter writer, string? value)
=> writer.WriteNullTerminatedString(value, Encoding.Unicode);
/// <summary>
/// Write a null-terminated UTF-32 string to the underlying stream
/// </summary>
public static bool WriteNullTerminatedUTF32String(this BinaryWriter writer, string? value)
=> writer.WriteNullTerminatedString(value, Encoding.UTF32);
/// <summary>
/// Write a byte-prefixed ASCII string to the underlying stream
/// </summary>
public static bool WritePrefixedAnsiString(this BinaryWriter writer, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.ASCII.GetBytes(value);
// Write the length as a byte
writer.Write((byte)value.Length);
// Write the buffer
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write a ushort-prefixed Unicode string to the underlying stream
/// </summary>
public static bool WritePrefixedUnicodeString(this BinaryWriter writer, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.Unicode.GetBytes(value);
// Write the length as a ushort
writer.Write((ushort)value.Length);
// Write the buffer
return WriteFromBuffer(writer, buffer);
}
/// <summary>
/// Write a <typeparamref name="T"/> to the underlying stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType<T>(this BinaryWriter writer, T? value)
=> writer.WriteType(value, typeof(T));
/// <summary>
/// Write a <typeparamref name="T"/> to the underlying stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType(this BinaryWriter writer, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return writer.Write((Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
{
writer.Write((Half)value);
return true;
}
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return writer.Write((Int128)value);
else if (type == typeof(UInt128))
return writer.Write((UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(writer, value, type);
else if (type.IsValueType && type.IsEnum)
return WriteNormalType(writer, value, Enum.GetUnderlyingType(type));
else
return WriteNormalType(writer, value, type);
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteNormalType(BinaryWriter writer, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
int typeSize = Marshal.SizeOf(type);
if (value.GetType() != type)
value = Convert.ChangeType(value, type);
var buffer = new byte[typeSize];
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
handle.Free();
return WriteFromBuffer(writer, buffer);
}
catch
{
return false;
}
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteComplexType(BinaryWriter writer, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
// Get the layout information
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
// Cache the current offset
long currentOffset = writer.BaseStream.Position;
// Generate the fields by parent first
var fields = MarshalHelpers.GetFields(type);
// Loop through the fields and set them
foreach (var fi in fields)
{
// If we have an explicit layout, move accordingly
if (layoutKind == LayoutKind.Explicit)
{
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
writer.BaseStream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
}
if (!GetField(writer, encoding, fields, value, fi))
return false;
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Write a single field from an object
/// </summary>
private static bool GetField(BinaryWriter writer, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
{
if (fi.FieldType.IsAssignableFrom(typeof(string)))
{
return WriteStringType(writer, encoding, instance, fi);
}
else if (fi.FieldType.IsArray)
{
return WriteArrayType(writer, fields, instance, fi);
}
else
{
var value = fi.GetValue(instance);
return writer.WriteType(value, fi.FieldType);
}
}
/// <summary>
/// Write an array type field from an object
/// </summary>
private static bool WriteArrayType(BinaryWriter writer, FieldInfo[] fields, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
if (marshalAsAttr == null)
return false;
// Get the array
Array? arr = fi.GetValue(instance) as Array;
if (arr == null)
return false;
// Get the number of elements expected
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
if (elementCount < 0)
return false;
// Get the item type for the array
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
// Loop through and write the array
for (int i = 0; i < elementCount; i++)
{
var value = arr.GetValue(i);
if (!WriteType(writer, value, elementType))
return false;
}
return true;
}
/// <summary>
/// Write a string type field from an object
/// </summary>
private static bool WriteStringType(BinaryWriter writer, Encoding encoding, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
string? fieldValue = fi.GetValue(instance) as string;
if (fieldValue == null)
return true;
switch (marshalAsAttr?.Value)
{
case UnmanagedType.AnsiBStr:
return writer.WritePrefixedAnsiString(fieldValue);
case UnmanagedType.BStr:
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
return writer.WritePrefixedUnicodeString(fieldValue);
case UnmanagedType.ByValTStr:
int byvalLength = marshalAsAttr!.SizeConst;
byte[] byvalBytes = encoding.GetBytes(fieldValue);
byte[] byvalSizedBytes = new byte[byvalLength];
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
writer.Write(byvalSizedBytes);
return true;
case UnmanagedType.LPStr:
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
case null:
return writer.WriteNullTerminatedAnsiString(fieldValue);
#if NET472_OR_GREATER || NETCOREAPP
case UnmanagedType.LPUTF8Str:
return writer.WriteNullTerminatedUTF8String(fieldValue);
#endif
case UnmanagedType.LPWStr:
return writer.WriteNullTerminatedUnicodeString(fieldValue);
// No other string types are recognized
default:
return false;
}
}
/// <summary>
/// Write an array of bytes to the underlying stream
/// </summary>
private static bool WriteFromBuffer(BinaryWriter writer, byte[] value)
{
// If the stream is not writable
if (!writer.BaseStream.CanWrite)
return false;
// Handle the 0-byte case
if (value.Length == 0)
return true;
// Handle the general case, forcing a write of the correct length
writer.Write(value, 0, value.Length);
return true;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
namespace SabreTools.IO.Extensions
{
public static class ByteArrayExtensions
{
/// <summary>
/// Indicates whether the specified array is null or has a length of zero
/// </summary>
public static bool IsNullOrEmpty(this Array? array)
{
return array == null || array.Length == 0;
}
/// <summary>
/// Convert a byte array to a hex string
/// </summary>
public static string? ToHexString(this byte[]? bytes)
{
// If we get null in, we send null out
if (bytes == null)
return null;
string hex = BitConverter.ToString(bytes);
return hex.Replace("-", string.Empty).ToLowerInvariant();
}
/// <summary>
/// Convert a hex string to a byte array
/// </summary>
public static byte[]? FromHexString(this string? hex)
{
// If we get null in, we send null out
if (string.IsNullOrEmpty(hex))
return null;
try
{
int chars = hex!.Length;
byte[] bytes = new byte[chars / 2];
for (int i = 0; i < chars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
catch
{
return null;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,798 @@
using System;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for byte arrays
/// </summary>
/// TODO: Handle proper negative values for Int24 and Int48
public static class ByteArrayWriterExtensions
{
/// <summary>
/// Write a UInt8 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, byte value)
=> WriteFromBuffer(content, ref offset, [value]);
/// <summary>
/// Write a UInt8[] and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, byte[] value)
=> WriteFromBuffer(content, ref offset, value);
/// <summary>
/// Write a UInt8[] and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, byte[] value)
{
Array.Reverse(value);
return WriteFromBuffer(content, ref offset, value);
}
/// <summary>
/// Write an Int8 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, sbyte value)
=> WriteFromBuffer(content, ref offset, [(byte)value]);
/// <summary>
/// Write a Char and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, char value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Char with an Encoding and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, char value, Encoding encoding)
{
byte[] buffer = encoding.GetBytes($"{value}");
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write an Int16 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, short value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write an Int16 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, short value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt16 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, ushort value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt16 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, ushort value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <summary>
/// Write a Half and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, Half value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Half and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, Half value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
#endif
/// <summary>
/// Write an Int32 as an Int24 and increment the pointer to an array
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24(this byte[] content, ref int offset, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write an Int32 as an Int24 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24BigEndian(this byte[] content, ref int offset, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24 and increment the pointer to an array
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24(this byte[] content, ref int offset, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24BigEndian(this byte[] content, ref int offset, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write an Int32 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write an Int32 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt32 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt32 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Single and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, float value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Single and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, float value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write an Int64 as an Int48 and increment the pointer to an array
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48(this byte[] content, ref int offset, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write an Int64 as an Int48 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48BigEndian(this byte[] content, ref int offset, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48 and increment the pointer to an array
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48(this byte[] content, ref int offset, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48BigEndian(this byte[] content, ref int offset, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(content, ref offset, reduced);
}
/// <summary>
/// Write an Int64 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write an Int64 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt64 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a UInt64 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Double and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, double value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Double and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, double value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Decimal and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, decimal value)
{
int[] bits = decimal.GetBits(value);
byte[] lo = BitConverter.GetBytes(bits[0]);
byte[] mid = BitConverter.GetBytes(bits[1]);
byte[] hi = BitConverter.GetBytes(bits[2]);
byte[] flags = BitConverter.GetBytes(bits[3]);
byte[] buffer = new byte[16];
Array.Copy(lo, 0, buffer, 0, 4);
Array.Copy(mid, 0, buffer, 4, 4);
Array.Copy(hi, 0, buffer, 8, 4);
Array.Copy(flags, 0, buffer, 12, 4);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Decimal and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, decimal value)
{
int[] bits = decimal.GetBits(value);
byte[] lo = BitConverter.GetBytes(bits[0]);
byte[] mid = BitConverter.GetBytes(bits[1]);
byte[] hi = BitConverter.GetBytes(bits[2]);
byte[] flags = BitConverter.GetBytes(bits[3]);
byte[] buffer = new byte[16];
Array.Copy(lo, 0, buffer, 0, 4);
Array.Copy(mid, 0, buffer, 4, 4);
Array.Copy(hi, 0, buffer, 8, 4);
Array.Copy(flags, 0, buffer, 12, 4);
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Guid and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, Guid value)
{
byte[] buffer = value.ToByteArray();
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a Guid and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, Guid value)
{
byte[] buffer = value.ToByteArray();
Array.Reverse(buffer);
return WriteFromBuffer(content, ref offset, buffer);
}
#if NET7_0_OR_GREATER
/// <summary>
/// Write an Int128 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(content, ref offset, padded);
}
/// <summary>
/// Write an Int128 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(content, ref offset, padded);
}
/// <summary>
/// Write a UInt128 and increment the pointer to an array
/// </summary>
public static bool Write(this byte[] content, ref int offset, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(content, ref offset, padded);
}
/// <summary>
/// Write a UInt128 and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this byte[] content, ref int offset, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(content, ref offset, padded);
}
#endif
/// <summary>
/// Write a null-terminated string to the array
/// </summary>
public static bool WriteNullTerminatedString(this byte[] content, ref int offset, string? value, Encoding encoding)
{
// If the value is null
if (value == null)
return false;
// Add the null terminator and write
value += "\0";
byte[] buffer = encoding.GetBytes(value);
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a null-terminated ASCII string to the byte array
/// </summary>
public static bool WriteNullTerminatedAnsiString(this byte[] content, ref int offset, string? value)
=> content.WriteNullTerminatedString(ref offset, value, Encoding.ASCII);
/// <summary>
/// Write a null-terminated UTF-8 string to the byte array
/// </summary>
public static bool WriteNullTerminatedUTF8String(this byte[] content, ref int offset, string? value)
=> content.WriteNullTerminatedString(ref offset, value, Encoding.UTF8);
/// <summary>
/// Write a null-terminated UTF-16 (Unicode) string to the byte array
/// </summary>
public static bool WriteNullTerminatedUnicodeString(this byte[] content, ref int offset, string? value)
=> content.WriteNullTerminatedString(ref offset, value, Encoding.Unicode);
/// <summary>
/// Write a null-terminated UTF-32 string to the byte array
/// </summary>
public static bool WriteNullTerminatedUTF32String(this byte[] content, ref int offset, string? value)
=> content.WriteNullTerminatedString(ref offset, value, Encoding.UTF32);
/// <summary>
/// Write a byte-prefixed ASCII string to the byte array
/// </summary>
public static bool WritePrefixedAnsiString(this byte[] content, ref int offset, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.ASCII.GetBytes(value);
// Write the length as a byte
if (!content.Write(ref offset, (byte)value.Length))
return false;
// Write the buffer
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a ushort-prefixed Unicode string to the byte array
/// </summary>
public static bool WritePrefixedUnicodeString(this byte[] content, ref int offset, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.Unicode.GetBytes(value);
// Write the length as a ushort
if (!content.Write(ref offset, (ushort)value.Length))
return false;
// Write the buffer
return WriteFromBuffer(content, ref offset, buffer);
}
/// <summary>
/// Write a <typeparamref name="T"/> to the byte array
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType<T>(this byte[] content, ref int offset, T? value)
=> content.WriteType(ref offset, value, typeof(T));
/// <summary>
/// Write a <typeparamref name="T"/> to the byte array
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType(this byte[] content, ref int offset, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return content.Write(ref offset, (Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return content.Write(ref offset, (Half)value);
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return content.Write(ref offset, (Int128)value);
else if (type == typeof(UInt128))
return content.Write(ref offset, (UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(content, ref offset, value, type);
else if (type.IsValueType && type.IsEnum)
return WriteNormalType(content, ref offset, value, Enum.GetUnderlyingType(type));
else
return WriteNormalType(content, ref offset, value, type);
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteNormalType(byte[] content, ref int offset, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
int typeSize = Marshal.SizeOf(type);
if (value.GetType() != type)
value = Convert.ChangeType(value, type);
var buffer = new byte[typeSize];
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
handle.Free();
return WriteFromBuffer(content, ref offset, buffer);
}
catch
{
return false;
}
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteComplexType(byte[] content, ref int offset, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
// Get the layout information
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
// Cache the current offset
int currentOffset = offset;
// Generate the fields by parent first
var fields = MarshalHelpers.GetFields(type);
// Loop through the fields and set them
foreach (var fi in fields)
{
// If we have an explicit layout, move accordingly
if (layoutKind == LayoutKind.Explicit)
{
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
offset = currentOffset + fieldOffset?.Value ?? 0;
}
if (!GetField(content, ref offset, encoding, fields, value, fi))
return false;
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Write a single field from an object
/// </summary>
private static bool GetField(byte[] content, ref int offset, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
{
if (fi.FieldType.IsAssignableFrom(typeof(string)))
{
return WriteStringType(content, ref offset, encoding, instance, fi);
}
else if (fi.FieldType.IsArray)
{
return WriteArrayType(content, ref offset, fields, instance, fi);
}
else
{
var value = fi.GetValue(instance);
return content.WriteType(ref offset, value, fi.FieldType);
}
}
/// <summary>
/// Write an array type field from an object
/// </summary>
private static bool WriteArrayType(byte[] content, ref int offset, FieldInfo[] fields, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
if (marshalAsAttr == null)
return false;
// Get the array
Array? arr = fi.GetValue(instance) as Array;
if (arr == null)
return false;
// Get the number of elements expected
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
if (elementCount < 0)
return false;
// Get the item type for the array
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
// Loop through and write the array
for (int i = 0; i < elementCount; i++)
{
var value = arr.GetValue(i);
if (!WriteType(content, ref offset, value, elementType))
return false;
}
return true;
}
/// <summary>
/// Write a string type field from an object
/// </summary>
private static bool WriteStringType(byte[] content, ref int offset, Encoding encoding, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
string? fieldValue = fi.GetValue(instance) as string;
if (fieldValue == null)
return true;
switch (marshalAsAttr?.Value)
{
case UnmanagedType.AnsiBStr:
return content.WritePrefixedAnsiString(ref offset, fieldValue);
case UnmanagedType.BStr:
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
return content.WritePrefixedUnicodeString(ref offset, fieldValue);
case UnmanagedType.ByValTStr:
int byvalLength = marshalAsAttr!.SizeConst;
byte[] byvalBytes = encoding.GetBytes(fieldValue);
byte[] byvalSizedBytes = new byte[byvalLength];
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
return content.Write(ref offset, byvalSizedBytes);
case UnmanagedType.LPStr:
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
case null:
return content.WriteNullTerminatedAnsiString(ref offset, fieldValue);
#if NET472_OR_GREATER || NETCOREAPP
case UnmanagedType.LPUTF8Str:
return content.WriteNullTerminatedUTF8String(ref offset, fieldValue);
#endif
case UnmanagedType.LPWStr:
return content.WriteNullTerminatedUnicodeString(ref offset, fieldValue);
// No other string types are recognized
default:
return false;
}
}
/// <summary>
/// Write an array of bytes to the byte array
/// </summary>
private static bool WriteFromBuffer(byte[] content, ref int offset, byte[] value)
{
// Handle the 0-byte case
if (value.Length == 0)
return true;
// If there are not enough bytes
if (offset + value.Length > content.Length)
throw new System.IO.EndOfStreamException(nameof(content));
// Handle the general case, forcing a write of the correct length
Array.Copy(value, 0, content, offset, value.Length);
offset += value.Length;
return true;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
namespace SabreTools.IO.Extensions
{
public static class EnumerableExtensions
{
/// <summary>
/// Safely iterate through an enumerable, skipping any errors
/// </summary>
public static IEnumerable<T> SafeEnumerate<T>(this IEnumerable<T> enumerable)
{
// Get the enumerator for the enumerable
IEnumerator<T> enumerator;
try
{
enumerator = enumerable.GetEnumerator();
}
catch
{
yield break;
}
// Iterate through and absorb any errors
while (true)
{
// Attempt to move to the next item
bool moved;
try
{
moved = enumerator.MoveNext();
}
catch (InvalidOperationException)
{
// Specific case for collections that were modified
yield break;
}
catch (System.IO.IOException ex) when (ex.Message.Contains("The file or directory is corrupted and unreadable."))
{
// Specific case we can't circumvent
yield break;
}
catch
{
continue;
}
// If the end of the enumeration is reached
if (!moved)
yield break;
// Return the next value from the enumeration
yield return enumerator.Current;
}
}
}
}

View File

@@ -0,0 +1,599 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Methods around path operations
/// </summary>
public static class IOExtensions
{
/// <summary>
/// Ensure the output directory is a proper format and can be created
/// </summary>
/// <param name="dir">Directory to check</param>
/// <param name="create">True if the directory should be created, false otherwise (default)</param>
/// <returns>Full path to the directory</returns>
public static string Ensure(this string? dir, bool create = false)
{
// If the output directory is invalid
if (string.IsNullOrEmpty(dir))
dir = PathTool.GetRuntimeDirectory();
// Get the full path for the output directory
dir = Path.GetFullPath(dir!.Trim('"'));
// If we're creating the output folder, do so
if (create && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
return dir;
}
/// <summary>
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
/// Defaults to ASCII when detection of the text file's endianness fails.
/// </summary>
/// <param name="filename">The text file to analyze.</param>
/// <returns>The detected encoding.</returns>
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
public static Encoding GetEncoding(this string filename)
{
if (string.IsNullOrEmpty(filename))
return Encoding.Default;
if (!File.Exists(filename))
return Encoding.Default;
// Try to open the file
try
{
var file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (file == null)
return Encoding.Default;
// Read the BOM
var bom = new byte[4];
int read = file.Read(bom, 0, 4);
file.Dispose();
// Disable warning about UTF7 usage
#pragma warning disable SYSLIB0001
// Analyze the BOM
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8;
if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE
if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
return Encoding.Default;
#pragma warning restore SYSLIB0001
}
catch
{
return Encoding.Default;
}
}
/// <summary>
/// Get the extension from the path, if possible
/// </summary>
/// <param name="path">Path to get extension from</param>
/// <returns>Extension, if possible</returns>
public static string? GetNormalizedExtension(this string? path)
{
// Check null or empty first
if (string.IsNullOrEmpty(path))
return null;
// Get the extension from the path, if possible
string? ext = Path.GetExtension(path)?.ToLowerInvariant();
// Check if the extension is null or empty
if (string.IsNullOrEmpty(ext))
return null;
// Make sure that extensions are valid
ext = ext!.TrimStart('.');
return ext;
}
/// <summary>
/// Get all empty folders within a root folder
/// </summary>
/// <param name="root">Root directory to parse</param>
/// <returns>IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise</returns>
public static List<string>? ListEmpty(this string? root)
{
// Check null or empty first
if (root == null)
return null;
// Then, check if the root exists
if (!Directory.Exists(root))
return null;
// Otherwise, get the complete list
var empty = new List<string>();
foreach (var dir in SafeGetDirectories(root, "*", SearchOption.AllDirectories))
{
if (SafeGetFiles(dir).Length == 0)
empty.Add(dir);
}
return empty;
}
#region Safe Directory Enumeration
/// <inheritdoc cref="Directory.GetDirectories(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path)
{
try
{
return Directory.GetDirectories(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path, string searchPattern)
{
try
{
return Directory.GetDirectories(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
return Directory.GetDirectories(path, searchPattern, searchOption);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path)
{
try
{
return Directory.GetFiles(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path, string searchPattern)
{
try
{
return Directory.GetFiles(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
return Directory.GetFiles(path, searchPattern, searchOption);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFileSystemEntries(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFileSystemEntries(this string path)
{
try
{
return Directory.GetFileSystemEntries(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFileSystemEntries(this string path, string searchPattern)
{
try
{
return Directory.GetFileSystemEntries(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.GetFileSystemEntries(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
#if NET20 || NET35
/// <inheritdoc cref="Directory.GetDirectories(string)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
=> path.SafeGetDirectories();
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
=> path.SafeGetDirectories(searchPattern);
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetDirectories(searchPattern, searchOption);
/// <inheritdoc cref="Directory.GetFiles(string)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
=> path.SafeGetFiles();
/// <inheritdoc cref="Directory.GetFiles(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
=> path.SafeGetFiles(searchPattern);
/// <inheritdoc cref="Directory.GetFiles(string, string, SearchOption)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetFiles(searchPattern, searchOption);
/// <inheritdoc cref="Directory.GetFileSystemEntries(string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
=> path.SafeGetFileSystemEntries();
/// <inheritdoc cref="Directory.GetFileSystemEntries(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
=> path.SafeGetFileSystemEntries(searchPattern);
/// <inheritdoc cref="Directory.GetFileSystemEntries(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetFileSystemEntries(searchPattern, searchOption);
#elif NET40_OR_GREATER
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
{
try
{
var enumerable = Directory.EnumerateDirectories(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateDirectories(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.EnumerateDirectories(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
{
try
{
var enumerable = Directory.EnumerateFiles(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateFiles(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.EnumerateFiles(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
{
try
{
var enumerable = Directory.EnumerateFileSystemEntries(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
#else
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <summary>Initializes a new instance of the <see cref="EnumerationOptions" /> class with the recommended default options.</summary>
/// <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/EnumerationOptions.cs#L42"</remarks>
private static EnumerationOptions FromSearchOption(SearchOption searchOption)
{
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new System.ArgumentOutOfRangeException(nameof(searchOption));
return searchOption == SearchOption.AllDirectories
? new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false }
: new EnumerationOptions { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false };
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Common methods for use during marshalling
/// </summary>
internal static class MarshalHelpers
{
/// <summary>
/// Get an attribute of the requested type
/// </summary>
public static T? GetAttribute<T>(FieldInfo? fi) where T : Attribute
{
// If the field info is invalid
if (fi == null)
return null;
// Get all matching attributes
var attributes = fi.GetCustomAttributes(typeof(T), true);
if (attributes == null || attributes.Length == 0)
return null;
// Get the first attribute that matches
return attributes[0] as T;
}
/// <summary>
/// Get an attribute of the requested type
/// </summary>
public static T? GetAttribute<T>(Type? type) where T : Attribute
{
// If the field info is invalid
if (type == null)
return null;
// Get all matching attributes
var attributes = type.GetCustomAttributes(typeof(T), true);
if (attributes == null || attributes.Length == 0)
return null;
// Get the first attribute that matches
return attributes[0] as T;
}
/// <summary>
/// Determine the layout kind for a type
/// </summary>
public static LayoutKind DetermineLayoutKind(StructLayoutAttribute? layoutAttr, Type type)
{
LayoutKind layoutKind = LayoutKind.Auto;
if (layoutAttr != null)
layoutKind = layoutAttr.Value;
else if (type.IsAutoLayout)
layoutKind = LayoutKind.Auto;
else if (type.IsExplicitLayout)
layoutKind = LayoutKind.Explicit;
else if (type.IsLayoutSequential)
layoutKind = LayoutKind.Sequential;
return layoutKind;
}
/// <summary>
/// Determine the encoding for a type
/// </summary>
public static Encoding DetermineEncoding(StructLayoutAttribute? layoutAttr)
{
return layoutAttr?.CharSet switch
{
CharSet.None => Encoding.ASCII,
CharSet.Ansi => Encoding.ASCII,
CharSet.Unicode => Encoding.Unicode,
CharSet.Auto => Encoding.ASCII, // UTF-8 on Unix
_ => Encoding.ASCII,
};
}
/// <summary>
/// Determine the parent hierarchy for a given type
/// </summary>
/// <remarks>Returns the highest parent as the first element</remarks>
public static IEnumerable<Type> DetermineTypeLineage(Type type)
{
var lineage = new List<Type>();
while (type != typeof(object) && type != typeof(ValueType))
{
lineage.Add(type);
type = type.BaseType ?? typeof(object);
}
lineage.Reverse();
return lineage;
}
/// <summary>
/// Get an ordered set of fields from a type
/// </summary>
/// <remarks>Returns fields from the parents before fields from the type</remarks>
public static FieldInfo[] GetFields(Type type)
{
// Get the type hierarchy for ensuring serialization order
var lineage = DetermineTypeLineage(type);
// Generate the fields by parent first
var fieldsList = new List<FieldInfo>();
foreach (var nextType in lineage)
{
var nextFields = nextType.GetFields();
foreach (var field in nextFields)
{
// Add fields if they aren't already included
int index = fieldsList.FindIndex(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (index == -1)
fieldsList.Add(field);
}
}
return [.. fieldsList];
}
/// <summary>
/// Get the expected array size for a field
/// </summary>
/// <returns>Array size on success, -1 on failure</returns>
public static int GetArrayElementCount(MarshalAsAttribute marshalAsAttr, FieldInfo[] fields, object instance)
{
int elementCount = -1;
if (marshalAsAttr.Value == UnmanagedType.ByValArray)
{
elementCount = marshalAsAttr.SizeConst;
}
else if (marshalAsAttr.Value == UnmanagedType.LPArray)
{
elementCount = marshalAsAttr.SizeConst;
if (marshalAsAttr.SizeParamIndex >= 0)
elementCount = GetLPArraySizeFromField(marshalAsAttr, fields, instance);
}
return elementCount;
}
/// <summary>
/// Get the expected LPArray size from a field
/// </summary>
public static int GetLPArraySizeFromField(MarshalAsAttribute marshalAsAttr, FieldInfo[] fields, object instance)
{
// If the index is invalid
if (marshalAsAttr.SizeParamIndex < 0)
return -1;
// Get the size field
var sizeField = fields[marshalAsAttr.SizeParamIndex];
if (sizeField == null)
return -1;
// Cast based on the field type
return sizeField.GetValue(instance) switch
{
sbyte val => val,
byte val => val,
short val => val,
ushort val => val,
int val => val,
uint val => (int)val,
long val => (int)val,
ulong val => (int)val,
_ => -1,
};
}
}
}

View File

@@ -0,0 +1,66 @@
using System.IO;
namespace SabreTools.IO.Extensions
{
public static class StreamExtensions
{
/// <summary>
/// Align the stream position to a byte-size boundary
/// </summary>
/// <param name="input">Input stream to try aligning</param>
/// <param name="alignment">Number of bytes to align on</param>
/// <returns>True if the stream could be aligned, false otherwise</returns>
public static bool AlignToBoundary(this Stream? input, byte alignment)
{
// If the stream is invalid
if (input == null || input.Length == 0 || !input.CanRead)
return false;
// If already at the end of the stream
if (input.Position >= input.Length)
return false;
// Align the stream position
while (input.Position % alignment != 0 && input.Position < input.Length)
{
_ = input.ReadByteValue();
}
// Return if the alignment completed
return input.Position % alignment == 0;
}
/// <summary>
/// Seek to a specific point in the stream, if possible
/// </summary>
/// <param name="input">Input stream to try seeking on</param>
/// <param name="offset">Optional offset to seek to</param>
public static long SeekIfPossible(this Stream input, long offset = 0)
{
// If the input is not seekable, just return the current position
if (!input.CanSeek)
{
try
{
return input.Position;
}
catch
{
return -1;
}
}
// Attempt to seek to the offset
try
{
if (offset < 0)
return input.Seek(offset, SeekOrigin.End);
else
return input.Seek(offset, SeekOrigin.Begin);
}
catch
{
return -1;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,798 @@
using System;
using System.IO;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for Streams
/// </summary>
/// TODO: Handle proper negative values for Int24 and Int48
public static class StreamWriterExtensions
{
/// <summary>
/// Write a UInt8
/// </summary>
public static bool Write(this Stream stream, byte value)
=> WriteFromBuffer(stream, [value]);
/// <summary>
/// Write a UInt8[]
/// </summary>
public static bool Write(this Stream stream, byte[] value)
=> WriteFromBuffer(stream, value);
/// <summary>
/// Write a UInt8[]
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, byte[] value)
{
Array.Reverse(value);
return WriteFromBuffer(stream, value);
}
/// <summary>
/// Write an Int8
/// </summary>
public static bool Write(this Stream stream, sbyte value)
=> WriteFromBuffer(stream, [(byte)value]);
/// <summary>
/// Write a Char
/// </summary>
public static bool Write(this Stream stream, char value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Char with an Encoding
/// </summary>
public static bool Write(this Stream stream, char value, Encoding encoding)
{
byte[] buffer = encoding.GetBytes($"{value}");
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write an Int16
/// </summary>
public static bool Write(this Stream stream, short value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write an Int16
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, short value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt16
/// </summary>
public static bool Write(this Stream stream, ushort value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt16
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, ushort value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <summary>
/// Write a Half
/// </summary>
public static bool Write(this Stream stream, Half value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Half
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, Half value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
#endif
/// <summary>
/// Write an Int32 as an Int24
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24(this Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write an Int32 as an Int24
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsInt24BigEndian(this Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24
/// </summary>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24(this Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[3];
Array.Copy(buffer, reduced, 3);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write a UInt32 as a UInt24
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top byte</remarks>
public static bool WriteAsUInt24BigEndian(this Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[3];
Array.Copy(buffer, 1, reduced, 0, 3);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write an Int32
/// </summary>
public static bool Write(this Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write an Int32
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt32
/// </summary>
public static bool Write(this Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt32
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Single
/// </summary>
public static bool Write(this Stream stream, float value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Single
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, float value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write an Int64 as an Int48
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48(this Stream stream, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write an Int64 as an Int48
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsInt48BigEndian(this Stream stream, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48
/// </summary>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48(this Stream stream, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
byte[] reduced = new byte[6];
Array.Copy(buffer, reduced, 6);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write a UInt64 as a UInt48
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
/// <remarks>Throws away top 2 bytes</remarks>
public static bool WriteAsUInt48BigEndian(this Stream stream, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
byte[] reduced = new byte[6];
Array.Copy(buffer, 2, reduced, 0, 6);
return WriteFromBuffer(stream, reduced);
}
/// <summary>
/// Write an Int64
/// </summary>
public static bool Write(this Stream stream, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write an Int64
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, long value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt64
/// </summary>
public static bool Write(this Stream stream, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a UInt64
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, ulong value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Double
/// </summary>
public static bool Write(this Stream stream, double value)
{
byte[] buffer = BitConverter.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Double
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, double value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Decimal and increment the pointer to an array
/// </summary>
public static bool Write(this Stream stream, decimal value)
{
int[] bits = decimal.GetBits(value);
byte[] lo = BitConverter.GetBytes(bits[0]);
byte[] mid = BitConverter.GetBytes(bits[1]);
byte[] hi = BitConverter.GetBytes(bits[2]);
byte[] flags = BitConverter.GetBytes(bits[3]);
byte[] buffer = new byte[16];
Array.Copy(lo, 0, buffer, 0, 4);
Array.Copy(mid, 0, buffer, 4, 4);
Array.Copy(hi, 0, buffer, 8, 4);
Array.Copy(flags, 0, buffer, 12, 4);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Decimal and increment the pointer to an array
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, decimal value)
{
int[] bits = decimal.GetBits(value);
byte[] lo = BitConverter.GetBytes(bits[0]);
byte[] mid = BitConverter.GetBytes(bits[1]);
byte[] hi = BitConverter.GetBytes(bits[2]);
byte[] flags = BitConverter.GetBytes(bits[3]);
byte[] buffer = new byte[16];
Array.Copy(lo, 0, buffer, 0, 4);
Array.Copy(mid, 0, buffer, 4, 4);
Array.Copy(hi, 0, buffer, 8, 4);
Array.Copy(flags, 0, buffer, 12, 4);
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Guid
/// </summary>
public static bool Write(this Stream stream, Guid value)
{
byte[] buffer = value.ToByteArray();
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a Guid
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, Guid value)
{
byte[] buffer = value.ToByteArray();
Array.Reverse(buffer);
return WriteFromBuffer(stream, buffer);
}
#if NET7_0_OR_GREATER
/// <summary>
/// Write an Int128
/// </summary>
public static bool Write(this Stream stream, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(stream, padded);
}
/// <summary>
/// Write an Int128
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, Int128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(stream, padded);
}
/// <summary>
/// Write a UInt128
/// </summary>
public static bool Write(this Stream stream, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(stream, padded);
}
/// <summary>
/// Write a UInt128
/// </summary>
/// <remarks>Writes in big-endian format</remarks>
public static bool WriteBigEndian(this Stream stream, UInt128 value)
{
byte[] buffer = ((BigInteger)value).ToByteArray();
Array.Reverse(buffer);
byte[] padded = new byte[16];
Array.Copy(buffer, 0, padded, 16 - buffer.Length, buffer.Length);
return WriteFromBuffer(stream, padded);
}
#endif
/// <summary>
/// Write a null-terminated string to the stream
/// </summary>
public static bool WriteNullTerminatedString(this Stream stream, string? value, Encoding encoding)
{
// If the value is null
if (value == null)
return false;
// Add the null terminator and write
value += "\0";
byte[] buffer = encoding.GetBytes(value);
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a null-terminated ASCII string to the stream
/// </summary>
public static bool WriteNullTerminatedAnsiString(this Stream stream, string? value)
=> stream.WriteNullTerminatedString(value, Encoding.ASCII);
/// <summary>
/// Write a null-terminated UTF-8 string to the stream
/// </summary>
public static bool WriteNullTerminatedUTF8String(this Stream stream, string? value)
=> stream.WriteNullTerminatedString(value, Encoding.UTF8);
/// <summary>
/// Write a null-terminated UTF-16 (Unicode) string to the stream
/// </summary>
public static bool WriteNullTerminatedUnicodeString(this Stream stream, string? value)
=> stream.WriteNullTerminatedString(value, Encoding.Unicode);
/// <summary>
/// Write a null-terminated UTF-32 string to the stream
/// </summary>
public static bool WriteNullTerminatedUTF32String(this Stream stream, string? value)
=> stream.WriteNullTerminatedString(value, Encoding.UTF32);
//// <summary>
/// Write a byte-prefixed ASCII string to the stream
/// </summary>
public static bool WritePrefixedAnsiString(this Stream stream, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.ASCII.GetBytes(value);
// Write the length as a byte
if (!stream.Write((byte)value.Length))
return false;
// Write the buffer
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a ushort-prefixed Unicode string to the stream
/// </summary>
public static bool WritePrefixedUnicodeString(this Stream stream, string? value)
{
// If the value is null
if (value == null)
return false;
// Get the buffer
byte[] buffer = Encoding.Unicode.GetBytes(value);
// Write the length as a ushort
if (!stream.Write((ushort)value.Length))
return false;
// Write the buffer
return WriteFromBuffer(stream, buffer);
}
/// <summary>
/// Write a <typeparamref name="T"/> to the stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType<T>(this Stream stream, T? value)
=> stream.WriteType(value, typeof(T));
/// <summary>
/// Write a <typeparamref name="T"/> to the stream
/// </summary>
/// <remarks>
/// This method is different than standard marshalling in a few notable ways:
/// - Strings are written by value, not by reference
/// - Complex objects are written by value, not by reference
/// - Enumeration values are written by the underlying value type
/// - Arrays of the above are handled sequentially as above
/// - Inherited fields from parents are serialized BEFORE fields in the child
/// </remarks>
public static bool WriteType(this Stream stream, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return stream.Write((Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return stream.Write((Half)value);
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return stream.Write((Int128)value);
else if (type == typeof(UInt128))
return stream.Write((UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(stream, value, type);
else if (type.IsValueType && type.IsEnum)
return WriteNormalType(stream, value, Enum.GetUnderlyingType(type));
else
return WriteNormalType(stream, value, type);
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteNormalType(Stream stream, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
int typeSize = Marshal.SizeOf(type);
if (value.GetType() != type)
value = Convert.ChangeType(value, type);
var buffer = new byte[typeSize];
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
handle.Free();
return WriteFromBuffer(stream, buffer);
}
catch
{
return false;
}
}
/// <summary>
/// Read a <paramref name="type"/> from the stream
/// </summary>
private static bool WriteComplexType(Stream stream, object? value, Type type)
{
try
{
// Null values cannot be written
if (value == null)
return true;
// Get the layout information
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
// Cache the current offset
long currentOffset = stream.Position;
// Generate the fields by parent first
var fields = MarshalHelpers.GetFields(type);
// Loop through the fields and set them
foreach (var fi in fields)
{
// If we have an explicit layout, move accordingly
if (layoutKind == LayoutKind.Explicit)
{
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
stream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
}
if (!GetField(stream, encoding, fields, value, fi))
return false;
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Write a single field from an object
/// </summary>
private static bool GetField(Stream stream, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
{
if (fi.FieldType.IsAssignableFrom(typeof(string)))
{
return WriteStringType(stream, encoding, instance, fi);
}
else if (fi.FieldType.IsArray)
{
return WriteArrayType(stream, fields, instance, fi);
}
else
{
var value = fi.GetValue(instance);
return stream.WriteType(value, fi.FieldType);
}
}
/// <summary>
/// Write an array type field from an object
/// </summary>
private static bool WriteArrayType(Stream stream, FieldInfo[] fields, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
if (marshalAsAttr == null)
return false;
// Get the array
Array? arr = fi.GetValue(instance) as Array;
if (arr == null)
return false;
// Get the number of elements expected
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
if (elementCount < 0)
return false;
// Get the item type for the array
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
// Loop through and write the array
for (int i = 0; i < elementCount; i++)
{
var value = arr.GetValue(i);
if (!WriteType(stream, value, elementType))
return false;
}
// Return the built array
return true;
}
/// <summary>
/// Write a string type field from an object
/// </summary>
private static bool WriteStringType(Stream stream, Encoding encoding, object instance, FieldInfo fi)
{
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
string? fieldValue = fi.GetValue(instance) as string;
if (fieldValue == null)
return true;
switch (marshalAsAttr?.Value)
{
case UnmanagedType.AnsiBStr:
return stream.WritePrefixedAnsiString(fieldValue);
case UnmanagedType.BStr:
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
return stream.WritePrefixedUnicodeString(fieldValue);
case UnmanagedType.ByValTStr:
int byvalLength = marshalAsAttr!.SizeConst;
byte[] byvalBytes = encoding.GetBytes(fieldValue);
byte[] byvalSizedBytes = new byte[byvalLength];
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
return Write(stream, byvalSizedBytes);
case UnmanagedType.LPStr:
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
case null:
return stream.WriteNullTerminatedAnsiString(fieldValue);
#if NET472_OR_GREATER || NETCOREAPP
case UnmanagedType.LPUTF8Str:
return stream.WriteNullTerminatedUTF8String(fieldValue);
#endif
case UnmanagedType.LPWStr:
return stream.WriteNullTerminatedUnicodeString(fieldValue);
// No other string types are recognized
default:
return false;
}
}
/// <summary>
/// Write an array of bytes to the stream
/// </summary>
private static bool WriteFromBuffer(Stream stream, byte[] value)
{
// If the stream is not writable
if (!stream.CanWrite)
return false;
// Handle the 0-byte case
if (value.Length == 0)
return true;
// Handle the general case, forcing a write of the correct length
stream.Write(value, 0, value.Length);
return true;
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Xml;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Additional methods for XmlTextWriter
@@ -15,7 +15,7 @@ namespace SabreTools.IO
/// <param name="localName">Name of the element</param>
/// <param name="value">Value to write in the element</param>
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
public static void WriteRequiredAttributeString(this XmlTextWriter writer, string localName, string value, bool throwOnError = false)
public static void WriteRequiredAttributeString(this XmlTextWriter writer, string localName, string? value, bool throwOnError = false)
{
// Throw an exception if we are configured to
if (value == null && throwOnError)
@@ -31,7 +31,7 @@ namespace SabreTools.IO
/// <param name="localName">Name of the element</param>
/// <param name="value">Value to write in the element</param>
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
public static void WriteRequiredElementString(this XmlTextWriter writer, string localName, string value, bool throwOnError = false)
public static void WriteRequiredElementString(this XmlTextWriter writer, string localName, string? value, bool throwOnError = false)
{
// Throw an exception if we are configured to
if (value == null && throwOnError)
@@ -51,7 +51,7 @@ namespace SabreTools.IO
/// <param name="writer">XmlTextWriter to write out with</param>
/// <param name="localName">Name of the attribute</param>
/// <param name="value">Value to write in the attribute</param>
public static void WriteOptionalAttributeString(this XmlTextWriter writer, string localName, string value)
public static void WriteOptionalAttributeString(this XmlTextWriter writer, string localName, string? value)
{
if (!string.IsNullOrEmpty(value))
writer.WriteAttributeString(localName, value);
@@ -63,7 +63,7 @@ namespace SabreTools.IO
/// <param name="writer">XmlTextWriter to write out with</param>
/// <param name="localName">Name of the element</param>
/// <param name="value">Value to write in the element</param>
public static void WriteOptionalElementString(this XmlTextWriter writer, string localName, string value)
public static void WriteOptionalElementString(this XmlTextWriter writer, string localName, string? value)
{
if (!string.IsNullOrEmpty(value))
writer.WriteElementString(localName, value);

View File

@@ -1,140 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO
{
/// <summary>
/// Methods around path operations
/// </summary>
public static class IOExtensions
{
/// <summary>
/// Ensure the output directory is a proper format and can be created
/// </summary>
/// <param name="dir">Directory to check</param>
/// <param name="create">True if the directory should be created, false otherwise (default)</param>
/// <returns>Full path to the directory</returns>
public static string Ensure(this string? dir, bool create = false)
{
// If the output directory is invalid
if (string.IsNullOrEmpty(dir))
dir = PathTool.GetRuntimeDirectory();
// Get the full path for the output directory
dir = Path.GetFullPath(dir!.Trim('"'));
// If we're creating the output folder, do so
if (create && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
return dir;
}
/// <summary>
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
/// Defaults to ASCII when detection of the text file's endianness fails.
/// </summary>
/// <param name="filename">The text file to analyze.</param>
/// <returns>The detected encoding.</returns>
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
public static Encoding GetEncoding(this string filename)
{
if (string.IsNullOrEmpty(filename))
return Encoding.Default;
if (!File.Exists(filename))
return Encoding.Default;
// Try to open the file
try
{
FileStream file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (file == null)
return Encoding.Default;
// Read the BOM
var bom = new byte[4];
file.Read(bom, 0, 4);
file.Dispose();
// Disable warning about UTF7 usage
#pragma warning disable SYSLIB0001
// Analyze the BOM
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8;
if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE
if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
return Encoding.Default;
#pragma warning restore SYSLIB0001
}
catch
{
return Encoding.Default;
}
}
/// <summary>
/// Get the extension from the path, if possible
/// </summary>
/// <param name="path">Path to get extension from</param>
/// <returns>Extension, if possible</returns>
public static string? GetNormalizedExtension(this string? path)
{
// Check null or empty first
if (string.IsNullOrEmpty(path))
return null;
// Get the extension from the path, if possible
string? ext = Path.GetExtension(path)?.ToLowerInvariant();
// Check if the extension is null or empty
if (string.IsNullOrEmpty(ext))
return null;
// Make sure that extensions are valid
ext = ext!.TrimStart('.');
return ext;
}
/// <summary>
/// Get all empty folders within a root folder
/// </summary>
/// <param name="root">Root directory to parse</param>
/// <returns>IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise</returns>
public static List<string>? ListEmpty(this string? root)
{
// Check null or empty first
if (string.IsNullOrEmpty(root))
return null;
// Then, check if the root exists
if (!Directory.Exists(root))
return null;
// If it does and it is empty, return a blank enumerable
#if NET20 || NET35
if (!Directory.GetFiles(root, "*", SearchOption.AllDirectories).Any())
#else
if (!Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Any())
#endif
return [];
// Otherwise, get the complete list
#if NET20 || NET35
return Directory.GetDirectories(root, "*", SearchOption.AllDirectories)
.Where(dir => !Directory.GetFiles(dir, "*", SearchOption.AllDirectories).Any())
.ToList();
#else
return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
.Where(dir => !Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Any())
.ToList();
#endif
}
}
}

View File

@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SabreTools.IO.Readers;
using SabreTools.IO.Writers;
@@ -14,13 +13,12 @@ namespace SabreTools.IO
/// </summary>
public class IniFile : IDictionary<string, string?>
{
private Dictionary<string, string?>? _keyValuePairs = [];
private readonly Dictionary<string, string?> _keyValuePairs = [];
public string? this[string? key]
{
get
{
_keyValuePairs ??= [];
key = key?.ToLowerInvariant() ?? string.Empty;
if (_keyValuePairs.ContainsKey(key))
return _keyValuePairs[key];
@@ -29,7 +27,6 @@ namespace SabreTools.IO
}
set
{
_keyValuePairs ??= [];
key = key?.ToLowerInvariant() ?? string.Empty;
_keyValuePairs[key] = value;
}
@@ -47,16 +44,19 @@ namespace SabreTools.IO
/// </summary>
public IniFile(string path)
{
this.Parse(path);
// If we don't have a file, we can't read it
if (!File.Exists(path))
throw new FileNotFoundException(nameof(path));
using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
Parse(fileStream);
}
/// <summary>
/// Populate an INI file from stream
/// </summary>
public IniFile(Stream stream)
{
this.Parse(stream);
}
=> Parse(stream);
/// <summary>
/// Add or update a key and value to the INI file
@@ -71,7 +71,7 @@ namespace SabreTools.IO
/// </summary>
public bool Remove(string key)
{
if (_keyValuePairs != null && _keyValuePairs.ContainsKey(key))
if (_keyValuePairs.ContainsKey(key))
{
_keyValuePairs.Remove(key.ToLowerInvariant());
return true;
@@ -81,22 +81,84 @@ namespace SabreTools.IO
}
/// <summary>
/// Read an INI file based on the path
/// Write an INI file to a path
/// </summary>
public bool Parse(string path)
public bool Write(string path)
{
// If we don't have a file, we can't read it
if (!File.Exists(path))
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs.Count == 0)
return false;
using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return Parse(fileStream);
using var fileStream = File.OpenWrite(path);
return Write(fileStream);
}
/// <summary>
/// Write an INI file to a stream
/// </summary>
public bool Write(Stream stream)
{
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs.Count == 0)
return false;
// If the stream is invalid, we can't output to it
if (!stream.CanWrite)
return false;
try
{
using IniWriter writer = new(stream, Encoding.UTF8);
// Order the keys to link sections together
var orderedKeys = new string[_keyValuePairs.Keys.Count];
_keyValuePairs.Keys.CopyTo(orderedKeys, 0);
Array.Sort(orderedKeys);
string section = string.Empty;
for (int i = 0; i < orderedKeys.Length; i++)
{
// Retrive the key and value
string key = orderedKeys[i];
string? value = _keyValuePairs[key];
// We assume '.' is a section name separator
if (key.Contains("."))
{
// Split the key by '.'
string[] data = key.Split('.');
// If the key contains an '.', we need to put them back in
string newSection = data[0].Trim();
string[] keyArr = new string[data.Length - 1];
Array.Copy(data, 1, keyArr, 0, keyArr.Length);
key = string.Join(".", keyArr).Trim();
// If we have a new section, write it out
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
{
writer.WriteSection(newSection);
section = newSection;
}
}
// Now write out the key and value in a standardized way
writer.WriteKeyValuePair(key, value);
}
}
catch
{
// We don't care what the error was, just catch and return
return false;
}
return true;
}
/// <summary>
/// Read an INI file from a stream
/// </summary>
public bool Parse(Stream? stream)
private bool Parse(Stream? stream)
{
// If the stream is invalid or unreadable, we can't process it
if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1)
@@ -148,82 +210,11 @@ namespace SabreTools.IO
return true;
}
/// <summary>
/// Write an INI file to a path
/// </summary>
public bool Write(string path)
{
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
return false;
using var fileStream = File.OpenWrite(path);
return Write(fileStream);
}
/// <summary>
/// Write an INI file to a stream
/// </summary>
public bool Write(Stream stream)
{
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
return false;
// If the stream is invalid or unwritable, we can't output to it
if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1)
return false;
try
{
using IniWriter writer = new(stream, Encoding.UTF8);
// Order the dictionary by keys to link sections together
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
string section = string.Empty;
foreach (var keyValuePair in orderedKeyValuePairs)
{
// Extract the key and value
string key = keyValuePair.Key;
string? value = keyValuePair.Value;
// We assume '.' is a section name separator
if (key.Contains("."))
{
// Split the key by '.'
string[] data = keyValuePair.Key.Split('.');
// If the key contains an '.', we need to put them back in
string newSection = data[0].Trim();
key = string.Join(".", data.Skip(1).ToArray()).Trim();
// If we have a new section, write it out
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
{
writer.WriteSection(newSection);
section = newSection;
}
}
// Now write out the key and value in a standardized way
writer.WriteKeyValuePair(key, value);
}
}
catch
{
// We don't care what the error was, just catch and return
return false;
}
return true;
}
#region IDictionary Impelementations
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? [];
public ICollection<string> Keys => _keyValuePairs.Keys;
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? [];
public ICollection<string?> Values => _keyValuePairs.Values;
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;

View File

@@ -0,0 +1,47 @@
namespace SabreTools.IO.Logging
{
public static class Converters
{
#region String to Enum
/// <summary>
/// Get the LogLevel value for an input string, if possible
/// </summary>
/// <param name="value">String value to parse/param>
/// <returns></returns>
public static LogLevel AsLogLevel(this string? value)
{
return value?.ToLowerInvariant() switch
{
"verbose" => LogLevel.VERBOSE,
"user" => LogLevel.USER,
"warning" => LogLevel.WARNING,
"error" => LogLevel.ERROR,
_ => LogLevel.VERBOSE,
};
}
#endregion
#region Enum to String
/// <summary>
/// Get string value from input LogLevel
/// </summary>
/// <param name="value">LogLevel to get value from</param>
/// <returns>String corresponding to the LogLevel</returns>
public static string? FromLogLevel(this LogLevel value)
{
return value switch
{
LogLevel.VERBOSE => "VERBOSE",
LogLevel.USER => "USER",
LogLevel.WARNING => "WARNING",
LogLevel.ERROR => "ERROR",
_ => null,
};
}
#endregion
}
}

View File

@@ -0,0 +1,13 @@
namespace SabreTools.IO.Logging
{
/// <summary>
/// Severity of the logging statement
/// </summary>
public enum LogLevel
{
VERBOSE = 0,
USER,
WARNING,
ERROR,
}
}

View File

@@ -0,0 +1,61 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Stopwatch class for keeping track of duration in the code
/// </summary>
public class InternalStopwatch
{
private string _subject;
private DateTime _startTime;
private readonly Logger _logger;
/// <summary>
/// Constructor that initalizes the stopwatch
/// </summary>
public InternalStopwatch()
{
_subject = string.Empty;
_logger = new Logger(this);
}
/// <summary>
/// Constructor that initalizes the stopwatch with a subject and starts immediately
/// </summary>
/// <param name="subject">Subject of the stopwatch</param>
public InternalStopwatch(string subject)
{
_subject = subject;
_logger = new Logger(this);
Start();
}
/// <summary>
/// Start the stopwatch and display subject text
/// </summary>
public void Start()
{
_startTime = DateTime.Now;
_logger.User($"{_subject}...");
}
/// <summary>
/// Start the stopwatch and display subject text
/// </summary>
/// <param name="subject">Text to show on stopwatch start</param>
public void Start(string subject)
{
_subject = subject;
Start();
}
/// <summary>
/// End the stopwatch and display subject text
/// </summary>
public void Stop()
{
_logger.User($"{_subject} completed in {DateTime.Now.Subtract(_startTime):G}");
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Generic delegate type for log events
/// </summary>
public delegate void LogEventHandler(object? sender, LogEventArgs args);
/// <summary>
/// Logging specific event arguments
/// </summary>
public class LogEventArgs : EventArgs
{
/// <summary>
/// LogLevel for the event
/// </summary>
public readonly LogLevel LogLevel;
/// <summary>
/// Log statement to be printed
/// </summary>
public readonly string? Statement = null;
/// <summary>
/// Exception to be passed along to the event handler
/// </summary>
public readonly Exception? Exception = null;
/// <summary>
/// Total count for progress log events
/// </summary>
public readonly long? TotalCount = null;
/// <summary>
/// Current count for progress log events
/// </summary>
public readonly long? CurrentCount = null;
/// <summary>
/// Statement constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, string statement)
{
LogLevel = logLevel;
Statement = statement;
}
/// <summary>
/// Statement constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, Exception exception)
{
LogLevel = logLevel;
Exception = exception;
}
/// <summary>
/// Statement and exception constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, string statement, Exception exception)
{
LogLevel = logLevel;
Statement = statement;
Exception = exception;
}
/// <summary>
/// Progress constructor
/// </summary>
public LogEventArgs(long total, long current, LogLevel logLevel, string? statement = null)
{
LogLevel = logLevel;
Statement = statement;
TotalCount = total;
CurrentCount = current;
}
}
}

View File

@@ -0,0 +1,180 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Per-class logging
/// </summary>
public class Logger
{
/// <summary>
/// Instance associated with this logger
/// </summary>
/// TODO: Derive class name for this object, if possible
private readonly object? _instance;
/// <summary>
/// Constructor
/// </summary>
public Logger(object? instance = null)
{
_instance = instance;
}
#region Log Event Triggers
#region Verbose
/// <summary>
/// Write the given string as a verbose message to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(string output)
=> LoggerImpl.Verbose(_instance, output);
/// <summary>
/// Write the given exception as a verbose message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(Exception ex)
=> LoggerImpl.Verbose(_instance, ex);
/// <summary>
/// Write the given exception and string as a verbose message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(Exception ex, string output)
=> LoggerImpl.Verbose(_instance, ex, output);
/// <summary>
/// Write the given verbose progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Verbose(long total, long current, string? output = null)
=> LoggerImpl.Verbose(_instance, total, current, output);
#endregion
#region User
/// <summary>
/// Write the given string as a user message to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(string output)
=> LoggerImpl.User(_instance, output);
/// <summary>
/// Write the given exception as a user message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(Exception ex)
=> LoggerImpl.User(_instance, ex);
/// <summary>
/// Write the given exception and string as a user message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(Exception ex, string output)
=> LoggerImpl.User(_instance, ex, output);
/// <summary>
/// Write the given user progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void User(long total, long current, string? output = null)
=> LoggerImpl.User(_instance, total, current, output);
#endregion
#region Warning
/// <summary>
/// Write the given string as a warning to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(string output)
=> LoggerImpl.Warning(_instance, output);
/// <summary>
/// Write the given exception as a warning to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(Exception ex)
=> LoggerImpl.Warning(_instance, ex);
/// <summary>
/// Write the given exception and string as a warning to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(Exception ex, string output)
=> LoggerImpl.Warning(_instance, ex, output);
/// <summary>
/// Write the given warning progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Warning(long total, long current, string? output = null)
=> LoggerImpl.Warning(_instance, total, current, output);
#endregion
#region Error
/// <summary>
/// Writes the given string as an error in the log
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(string output)
=> LoggerImpl.Error(_instance, output);
/// <summary>
/// Writes the given exception as an error in the log
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(Exception ex)
=> LoggerImpl.Error(_instance, ex);
/// <summary>
/// Writes the given exception and string as an error in the log
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(Exception ex, string output)
=> LoggerImpl.Error(_instance, ex, output);
/// <summary>
/// Write the given error progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Error(long total, long current, string? output = null)
=> LoggerImpl.Error(_instance, total, current, output);
#endregion
#endregion
}
}

View File

@@ -0,0 +1,458 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Internal logging implementation
/// </summary>
public static class LoggerImpl
{
#region Fields
/// <summary>
/// Optional output filename for logs
/// </summary>
public static string? Filename { get; private set; } = null;
/// <summary>
/// Determines if we're logging to file or not
/// </summary>
public static bool LogToFile { get { return !string.IsNullOrEmpty(Filename); } }
/// <summary>
/// Optional output log directory
/// </summary>
public static string? LogDirectory { get; private set; } = null;
/// <summary>
/// Determines the lowest log level to output
/// </summary>
public static LogLevel LowestLogLevel { get; set; } = LogLevel.VERBOSE;
/// <summary>
/// Determines whether to prefix log lines with level and datetime
/// </summary>
public static bool AppendPrefix { get; set; } = true;
/// <summary>
/// Determines whether to throw if an exception is logged
/// </summary>
public static bool ThrowOnError { get; set; } = false;
/// <summary>
/// Logging start time for metrics
/// </summary>
public static DateTime StartTime { get; private set; }
/// <summary>
/// Determines if there were errors logged
/// </summary>
public static bool LoggedErrors { get; private set; } = false;
/// <summary>
/// Determines if there were warnings logged
/// </summary>
public static bool LoggedWarnings { get; private set; } = false;
#endregion
#region Private variables
/// <summary>
/// StreamWriter representing the output log file
/// </summary>
private static StreamWriter? _log;
/// <summary>
/// Object lock for multithreaded logging
/// </summary>
private static readonly object _lock = new();
#endregion
#region Control
/// <summary>
/// Generate and set the log filename
/// </summary>
/// <param name="filename">Base filename to use</param>
/// <param name="addDate">True to append a date to the filename, false otherwise</param>
public static void SetFilename(string filename, bool addDate = true)
{
// Get the full log path
string fullPath = Path.GetFullPath(filename);
// Set the log directory
LogDirectory = Path.GetDirectoryName(fullPath);
// Set the
if (addDate)
Filename = $"{Path.GetFileNameWithoutExtension(fullPath)} ({DateTime.Now:yyyy-MM-dd HH-mm-ss}).{fullPath.GetNormalizedExtension()}";
else
Filename = Path.GetFileName(fullPath);
}
/// <summary>
/// Start logging by opening output file (if necessary)
/// </summary>
/// <returns>True if the logging was started correctly, false otherwise</returns>
public static bool Start()
{
// Setup the logging handler to always use the internal log
LogEventHandler += HandleLogEvent;
// Start the logging
StartTime = DateTime.Now;
if (!LogToFile)
return true;
// Setup file output and perform initial log
try
{
if (!string.IsNullOrEmpty(LogDirectory) && !Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
FileStream logfile = File.Create(Path.Combine(LogDirectory ?? string.Empty, Filename ?? string.Empty));
#if NET20 || NET35 || NET40
_log = new StreamWriter(logfile, Encoding.UTF8, 4096)
#else
_log = new StreamWriter(logfile, Encoding.UTF8, 4096, true)
#endif
{
AutoFlush = true
};
_log.WriteLine($"Logging started {StartTime:yyyy-MM-dd HH:mm:ss}");
_log.WriteLine($"Command run: {string.Join(" ", Environment.GetCommandLineArgs())}");
}
catch
{
return false;
}
return true;
}
/// <summary>
/// End logging by closing output file (if necessary)
/// </summary>
/// <param name="suppress">True if all ending output is to be suppressed, false otherwise (default)</param>
/// <returns>True if the logging was ended correctly, false otherwise</returns>
public static bool Close(bool suppress = false)
{
if (!suppress)
{
if (LoggedWarnings)
Console.WriteLine("There were warnings in the last run! Check the log for more details");
if (LoggedErrors)
Console.WriteLine("There were errors in the last run! Check the log for more details");
TimeSpan span = DateTime.Now.Subtract(StartTime);
#if NET20 || NET35
string total = span.ToString();
#else
// Special case for multi-day runs
string total;
if (span >= TimeSpan.FromDays(1))
total = span.ToString(@"d\:hh\:mm\:ss");
else
total = span.ToString(@"hh\:mm\:ss");
#endif
if (!LogToFile)
{
Console.WriteLine($"Total runtime: {total}");
return true;
}
try
{
_log?.WriteLine($"Logging ended {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_log?.WriteLine($"Total runtime: {total}");
Console.WriteLine($"Total runtime: {total}");
_log?.Close();
}
catch
{
return false;
}
}
else
{
try
{
_log?.Close();
}
catch
{
return false;
}
}
return true;
}
#endregion
#region Event Handling
/// <summary>
/// Handler for log events
/// </summary>
public static event LogEventHandler LogEventHandler = delegate { };
/// <summary>
/// Default log event handling
/// </summary>
public static void HandleLogEvent(object? sender, LogEventArgs args)
{
// Null args means we can't handle it
if (args == null)
return;
// If we have an exception and we're throwing on that
if (ThrowOnError && args.Exception != null)
throw args.Exception;
// If we have a warning or error, set the flags accordingly
if (args.LogLevel == LogLevel.WARNING)
LoggedWarnings = true;
if (args.LogLevel == LogLevel.ERROR)
LoggedErrors = true;
// Setup the statement based on the inputs
string logLine;
if (args.Exception != null)
{
logLine = $"{(args.Statement != null ? args.Statement + ": " : string.Empty)}{args.Exception}";
}
else if (args.TotalCount != null && args.CurrentCount != null)
{
double percentage = ((double)args.CurrentCount.Value / args.TotalCount.Value) * 100;
logLine = $"{percentage:N2}%{(args.Statement != null ? ": " + args.Statement : string.Empty)}";
}
else
{
logLine = args.Statement ?? string.Empty;
}
// Then write to the log
Log(logLine, args.LogLevel);
}
/// <summary>
/// Write the given string to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <param name="loglevel">Severity of the information being logged</param>
private static void Log(string output, LogLevel loglevel)
{
// If the log level is less than the filter level, we skip it but claim we didn't
if (loglevel < LowestLogLevel)
return;
// USER and ERROR writes to console
if (loglevel == LogLevel.USER || loglevel == LogLevel.ERROR)
Console.WriteLine((loglevel == LogLevel.ERROR && AppendPrefix ? loglevel.FromLogLevel() + " " : string.Empty) + output);
// If we're writing to file, use the existing stream
if (LogToFile)
{
try
{
lock (_lock)
{
_log?.WriteLine((AppendPrefix ? $"{loglevel.FromLogLevel()} - {DateTime.Now} - " : string.Empty) + output);
}
}
catch (Exception ex) when (ThrowOnError)
{
Console.WriteLine(ex);
Console.WriteLine("Could not write to log file!");
return;
}
}
return;
}
#endregion
#region Log Event Triggers
#region Verbose
/// <summary>
/// Write the given string as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output));
/// <summary>
/// Write the given exception as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, ex));
/// <summary>
/// Write the given exception and string as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output, ex));
/// <summary>
/// Write the given verbose progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Verbose(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.VERBOSE, output));
#endregion
#region User
/// <summary>
/// Write the given string as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output));
/// <summary>
/// Write the given exception as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, ex));
/// <summary>
/// Write the given exception and string as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output, ex));
/// <summary>
/// Write the given user progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void User(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.USER, output));
#endregion
#region Warning
/// <summary>
/// Write the given string as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output));
/// <summary>
/// Write the given exception as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, ex));
//// <summary>
/// Write the given exception and string as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output, ex));
/// <summary>
/// Write the given warning progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Warning(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.WARNING, output));
#endregion
#region Error
/// <summary>
/// Writes the given string as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output));
/// <summary>
/// Writes the given exception as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, ex));
/// <summary>
/// Writes the given exception and string as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output, ex));
/// <summary>
/// Write the given error progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Error(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.ERROR, output));
#endregion
#endregion
}
}

View File

@@ -11,12 +11,12 @@ namespace SabreTools.IO
/// <summary>
/// Current full path represented
/// </summary>
public string CurrentPath { get; private set; }
public string CurrentPath { get; }
/// <summary>
/// Possible parent path represented (may be null or empty)
/// </summary>
public string? ParentPath { get; private set; }
public string? ParentPath { get; }
public ParentablePath(string currentPath, string? parentPath = null)
{
@@ -32,19 +32,19 @@ namespace SabreTools.IO
public string? GetNormalizedFileName(bool sanitize)
{
// If the current path is empty, we can't do anything
if (string.IsNullOrEmpty(CurrentPath))
if (CurrentPath.Length == 0)
return null;
// Assume the current path is the filename
string filename = Path.GetFileName(CurrentPath);
// If we have a true ParentPath, remove it from CurrentPath and return the remainder
if (!string.IsNullOrEmpty(ParentPath) && !PathsEqual(CurrentPath, ParentPath))
if (!string.IsNullOrEmpty(ParentPath) && !PathsEqual(CurrentPath, ParentPath))
filename = CurrentPath.Remove(0, ParentPath!.Length + 1);
// If we're sanitizing the path after, do so
if (sanitize)
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
filename = filename.Replace('\\', '-').Replace('/', '-');
return filename;
}
@@ -57,42 +57,45 @@ namespace SabreTools.IO
/// <returns>Complete output path</returns>
public string? GetOutputPath(string? outDir, bool inplace)
{
// If the current path is empty, we can't do anything
if (string.IsNullOrEmpty(CurrentPath))
// If the current path is empty
if (CurrentPath.Length == 0)
return null;
// If the output dir is empty (and we're not inplace), we can't do anything
outDir = outDir?.Trim();
if (string.IsNullOrEmpty(outDir) && !inplace)
return null;
// Check if we have a split path or not
bool splitpath = !string.IsNullOrEmpty(ParentPath);
// If we have an inplace output, use the directory name from the input path
// If we have an inplace output
if (inplace)
return Path.GetDirectoryName(CurrentPath);
// If the current and parent paths are the same, just use the output directory
if (!splitpath || CurrentPath.Length == (ParentPath?.Length ?? 0))
// If the output dir is empty after trimming
outDir = outDir?.Trim();
if (string.IsNullOrEmpty(outDir))
return null;
// If the parent path is empty or the paths are equal
if (string.IsNullOrEmpty(ParentPath) || PathsEqual(CurrentPath, ParentPath))
return outDir;
// By default, the working parent directory is the parent path
string workingParent = ParentPath ?? string.Empty;
string workingParent = ParentPath!;
// TODO: Should this be the default? Always create a subfolder if a folder is found?
// If we are processing a path that is coming from a directory and we are outputting to the current directory, we want to get the subfolder to write to
if (outDir == Environment.CurrentDirectory)
workingParent = Path.GetDirectoryName(ParentPath ?? string.Empty) ?? string.Empty;
workingParent = Path.GetDirectoryName(ParentPath) ?? string.Empty;
// Handle bizarre Windows-like paths on Linux
if (workingParent.EndsWith(":") && Path.DirectorySeparatorChar == '/')
workingParent += '/';
// Determine the correct subfolder based on the working parent directory
int extraLength = workingParent.EndsWith(":")
|| workingParent.EndsWith(Path.DirectorySeparatorChar.ToString())
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar.ToString()) ? 0 : 1;
|| workingParent.EndsWith("\\")
|| workingParent.EndsWith("/") ? 0 : 1;
return Path.GetDirectoryName(Path.Combine(outDir!, CurrentPath.Remove(0, workingParent.Length + extraLength)));
string strippedPath = CurrentPath.Remove(0, workingParent.Length + extraLength);
string combinedPath = Path.Combine(outDir!, strippedPath);
return Path.GetDirectoryName(combinedPath);
}
/// <summary>
/// Determine if two paths are equal or not
/// </summary>
@@ -123,8 +126,8 @@ namespace SabreTools.IO
if (input == null)
return null;
// Replace alternate directory separators with the correct one
return input.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
// Replace '\' with '/'
return input.Replace('\\', '/');
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.Matching;
using SabreTools.IO.Extensions;
using SabreTools.Matching.Compare;
namespace SabreTools.IO
{
@@ -13,9 +14,9 @@ namespace SabreTools.IO
/// Retrieve a list of just directories from inputs
/// </summary>
/// <param name="inputs">List of strings representing directories and files</param>
/// <param name="appendparent">True if the parent name should be included in the ParentablePath, false otherwise (default)</param>
/// <param name="appendParent">True if the parent name should be included in the ParentablePath, false otherwise (default)</param>
/// <returns>List of strings representing just directories from the inputs</returns>
public static List<ParentablePath> GetDirectoriesOnly(List<string> inputs, bool appendparent = false)
public static List<ParentablePath> GetDirectoriesOnly(List<string> inputs, bool appendParent = false)
{
var outputs = new List<ParentablePath>();
for (int i = 0; i < inputs.Count; i++)
@@ -41,7 +42,7 @@ namespace SabreTools.IO
List<string> directories = GetDirectoriesOrdered(input, pattern);
foreach (string dir in directories)
{
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendparent ? parentPath : string.Empty));
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendParent ? parentPath : string.Empty));
}
}
}
@@ -70,7 +71,7 @@ namespace SabreTools.IO
private static List<string> GetDirectoriesOrderedHelper(string dir, List<string> infiles, string pattern)
{
// Take care of the files in the top directory
List<string> toadd = [.. Directory.GetDirectories(dir, pattern, SearchOption.TopDirectoryOnly)];
List<string> toadd = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)];
toadd.Sort(new NaturalComparer());
infiles.AddRange(toadd);
@@ -88,9 +89,9 @@ namespace SabreTools.IO
/// Retrieve a list of just files from inputs
/// </summary>
/// <param name="inputs">List of strings representing directories and files</param>
/// <param name="appendparent">True if the parent name should be be included in the ParentablePath, false otherwise (default)</param>
/// <param name="appendParent">True if the parent name should be be included in the ParentablePath, false otherwise (default)</param>
/// <returns>List of strings representing just files from the inputs</returns>
public static List<ParentablePath> GetFilesOnly(List<string> inputs, bool appendparent = false)
public static List<ParentablePath> GetFilesOnly(List<string> inputs, bool appendParent = false)
{
var outputs = new List<ParentablePath>();
for (int i = 0; i < inputs.Count; i++)
@@ -116,12 +117,12 @@ namespace SabreTools.IO
List<string> files = GetFilesOrdered(input, pattern);
foreach (string file in files)
{
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendparent ? parentPath : string.Empty));
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendParent ? parentPath : string.Empty));
}
}
else if (File.Exists(input))
{
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendparent ? parentPath : string.Empty));
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendParent ? parentPath : string.Empty));
}
}
@@ -149,12 +150,12 @@ namespace SabreTools.IO
private static List<string> GetFilesOrderedHelper(string dir, List<string> infiles, string pattern)
{
// Take care of the files in the top directory
List<string> toadd = [.. Directory.GetFiles(dir, pattern, SearchOption.TopDirectoryOnly)];
List<string> toadd = [.. dir.SafeEnumerateFiles(pattern, SearchOption.TopDirectoryOnly)];
toadd.Sort(new NaturalComparer());
infiles.AddRange(toadd);
// Then recurse through and add from the directories
List<string> subDirs = [.. Directory.GetDirectories(dir, pattern, SearchOption.TopDirectoryOnly)];
List<string> subDirs = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)];
subDirs.Sort(new NaturalComparer());
foreach (string subdir in subDirs)
{

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -295,13 +294,10 @@ namespace SabreTools.IO.Readers
s = s.Trim();
// Now we get each string, divided up as cleanly as possible
string[] matches = Regex
.Matches(s, InternalPatternAttributesCMP)
.Cast<Match>()
.Select(m => m.Groups[0].Value)
.ToArray();
return matches;
var matchList = Regex.Matches(s, InternalPatternAttributesCMP);
var matchArr = new Match[matchList.Count];
matchList.CopyTo(matchArr, 0);
return Array.ConvertAll(matchArr, m => m.Groups[0].Value);
}
/// <summary>

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO.Readers
@@ -118,7 +117,9 @@ namespace SabreTools.IO.Readers
// If the value field contains an '=', we need to put them back in
string key = data[0].Trim();
string value = string.Join("=", data.Skip(1).ToArray()).Trim();
var valueArr = new string[data.Length - 1];
Array.Copy(data, 1, valueArr, 0, valueArr.Length);
string value = string.Join("=", valueArr).Trim();
KeyValuePair = new KeyValuePair<string, string>(key, value);
RowType = IniRowType.KeyValue;

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -124,7 +123,7 @@ namespace SabreTools.IO.Readers
// https://stackoverflow.com/questions/3776458/split-a-comma-separated-string-with-both-quoted-and-unquoted-strings
var lineSplitRegex = new Regex($"(?:^|{Separator})(\"(?:[^\"]+|\"\")*\"|[^{Separator}]*)");
var temp = new List<string>();
foreach (Match? match in lineSplitRegex.Matches(fullLine).Cast<Match?>())
foreach (Match? match in lineSplitRegex.Matches(fullLine))
{
string? curr = match?.Value;
if (curr == null)
@@ -143,7 +142,9 @@ namespace SabreTools.IO.Readers
// Otherwise, just split on the delimiter
else
{
Line = fullLine.Split(Separator).Select(f => f.Trim()).ToList();
var lineArr = fullLine.Split(Separator);
lineArr = Array.ConvertAll(lineArr, f => f.Trim());
Line = [.. lineArr];
}
// If we don't have a header yet and are expecting one, read this as the header

View File

@@ -1,37 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.2</Version>
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<NoWarn>CS0618</NoWarn>
<Nullable>enable</Nullable>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.6.3</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common IO utilities by other SabreTools projects</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.IO</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>io reading writing ini csv ssv tsv</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common IO utilities by other SabreTools projects</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2025</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.IO</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>io reading writing ini csv ssv tsv</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath=""/>
</ItemGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.5.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.3.1" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,272 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO
{
/// <summary>
/// Extensions for Streams
/// </summary>
/// <remarks>TODO: Add U/Int24 and U/Int48 methods</remarks>
public static class StreamExtensions
{
/// <summary>
/// Read a UInt8 from the stream
/// </summary>
public static byte ReadByteValue(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
return buffer[0];
}
/// <summary>
/// Read a UInt8[] from the stream
/// </summary>
public static byte[]? ReadBytes(this Stream stream, int count)
{
// If there's an invalid byte count, don't do anything
if (count <= 0)
return null;
byte[] buffer = new byte[count];
stream.Read(buffer, 0, count);
return buffer;
}
/// <summary>
/// Read a Int8 from the stream
/// </summary>
public static sbyte ReadSByte(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
return (sbyte)buffer[0];
}
/// <summary>
/// Read a Char from the stream
/// </summary>
public static char ReadChar(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
return (char)buffer[0];
}
/// <summary>
/// Read a Int16 from the stream
/// </summary>
public static short ReadInt16(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
return BitConverter.ToInt16(buffer, 0);
}
/// <summary>
/// Read a Int16 from the stream in big-endian format
/// </summary>
public static short ReadInt16BigEndian(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
Array.Reverse(buffer);
return BitConverter.ToInt16(buffer, 0);
}
/// <summary>
/// Read a UInt16 from the stream
/// </summary>
public static ushort ReadUInt16(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a UInt16 from the stream in big-endian format
/// </summary>
public static ushort ReadUInt16BigEndian(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
Array.Reverse(buffer);
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read an Int32 from the stream
/// </summary>
public static int ReadInt32(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
return BitConverter.ToInt32(buffer, 0);
}
/// <summary>
/// Read an Int32 from the stream in big-endian format
/// </summary>
public static int ReadInt32BigEndian(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
Array.Reverse(buffer);
return BitConverter.ToInt32(buffer, 0);
}
/// <summary>
/// Read a UInt32 from the stream
/// </summary>
public static uint ReadUInt32(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a UInt32 from the stream in big-endian format
/// </summary>
public static uint ReadUInt32BigEndian(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
Array.Reverse(buffer);
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a Int64 from the stream
/// </summary>
public static long ReadInt64(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
return BitConverter.ToInt64(buffer, 0);
}
/// <summary>
/// Read a Int64 from the stream in big-endian format
/// </summary>
public static long ReadInt64BigEndian(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
Array.Reverse(buffer);
return BitConverter.ToInt64(buffer, 0);
}
/// <summary>
/// Read a UInt64 from the stream
/// </summary>
public static ulong ReadUInt64(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
return BitConverter.ToUInt64(buffer, 0);
}
/// <summary>
/// Read a UInt64 from the stream in big-endian format
/// </summary>
public static ulong ReadUInt64BigEndian(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
Array.Reverse(buffer);
return BitConverter.ToUInt64(buffer, 0);
}
/// <summary>
/// Read a Guid from the stream
/// </summary>
public static Guid ReadGuid(this Stream stream)
{
byte[] buffer = new byte[16];
stream.Read(buffer, 0, 16);
return new Guid(buffer);
}
/// <summary>
/// Read a Guid from the stream in big-endian format
/// </summary>
public static Guid ReadGuidBigEndian(this Stream stream)
{
byte[] buffer = new byte[16];
stream.Read(buffer, 0, 16);
Array.Reverse(buffer);
return new Guid(buffer);
}
/// <summary>
/// Read a null-terminated string from the stream
/// </summary>
public static string? ReadString(this Stream stream) => stream.ReadString(Encoding.Default);
/// <summary>
/// Read a null-terminated string from the stream
/// </summary>
public static string? ReadString(this Stream stream, Encoding encoding)
{
if (stream.Position >= stream.Length)
return null;
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
int charWidth = nullTerminator.Length;
var tempBuffer = new List<byte>();
byte[] buffer = new byte[charWidth];
while (stream.Position < stream.Length && stream.Read(buffer, 0, charWidth) != 0 && !buffer.SequenceEqual(nullTerminator))
{
tempBuffer.AddRange(buffer);
}
return encoding.GetString([.. tempBuffer]);
}
/// <summary>
/// Seek to a specific point in the stream, if possible
/// </summary>
/// <param name="input">Input stream to try seeking on</param>
/// <param name="offset">Optional offset to seek to</param>
public static long SeekIfPossible(this Stream input, long offset = 0)
{
// If the stream is null, don't even try
if (input == null)
return -1;
// If the input is not seekable, just return the current position
if (!input.CanSeek)
{
try
{
return input.Position;
}
catch
{
return -1;
}
}
// Attempt to seek to the offset
try
{
if (offset < 0)
return input.Seek(offset, SeekOrigin.End);
else if (offset >= 0)
return input.Seek(offset, SeekOrigin.Begin);
return input.Position;
}
catch
{
return -1;
}
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Wrapper to allow reading bits from a source stream
/// </summary>
public class ReadOnlyBitStream
{
/// <inheritdoc cref="Stream.Position"/>
public long Position => _source.Position;
/// <inheritdoc cref="Stream.Length"/>
public long Length => _source.Length;
/// <summary>
/// Original stream source
/// </summary>
private readonly Stream _source;
/// <summary>
/// Last read byte value from the stream
/// </summary>
private byte? _bitBuffer;
/// <summary>
/// Index in the byte of the current bit
/// </summary>
private int _bitIndex;
/// <summary>
/// Create a new BitStream from a source Stream
/// </summary>
public ReadOnlyBitStream(Stream source)
{
_source = source;
_bitBuffer = null;
_bitIndex = 0;
// Verify the stream
if (!source.CanRead || !source.CanSeek)
throw new ArgumentException($"{nameof(source)} needs to be readable and seekable");
}
/// <summary>
/// Discard the current cached byte
/// </summary>
public void Discard()
{
_bitBuffer = null;
_bitIndex = 0;
}
/// <summary>
/// Read a single bit, if possible
/// </summary>
/// <returns>The next bit encoded in a byte, null on error or end of stream</returns>
public byte? ReadBit()
{
// If we reached the end of the stream
if (_source.Position >= _source.Length)
return null;
// If we don't have a value cached
if (_bitBuffer == null)
{
// Read the next byte, if possible
_bitBuffer = ReadSourceByte();
if (_bitBuffer == null)
return null;
// Reset the bit index
_bitIndex = 0;
}
// Get the value by bit-shifting
int value = _bitBuffer.Value & 0x01;
_bitBuffer = (byte?)(_bitBuffer >> 1);
_bitIndex++;
// Reset the byte if we're at the end
if (_bitIndex >= 8)
Discard();
return (byte)value;
}
/// <summary>
/// Read a multiple bits MSB-first, if possible
/// </summary>
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
/// <remarks>[76543210] order within a byte, appended to output [76543210]</remarks>
public uint? ReadBitsBE(int bits)
{
uint value = 0;
for (int i = 0; i < bits; i++)
{
// Read the next bit
byte? bitValue = ReadBit();
if (bitValue == null)
return null;
// Append the bit shifted by the current index
value |= (uint)(bitValue.Value << i);
}
return value;
}
/// <summary>
/// Read a multiple bits in LSB-first, if possible
/// </summary>
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
/// <remarks>[76543210] order within a byte, appended to output [01234567]</remarks>
public uint? ReadBitsLE(int bits)
{
uint value = 0;
for (int i = 0; i < bits; i++)
{
// Read the next bit
byte? bitValue = ReadBit();
if (bitValue == null)
return null;
// Append the bit shifted by the current index
value = (value << 1) | bitValue.Value;
}
return value;
}
/// <summary>
/// Read a byte, if possible
/// </summary>
/// <returns>The next byte, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public byte? ReadByte()
{
try
{
Discard();
return _source.ReadByteValue();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt16, if possible
/// </summary>
/// <returns>The next UInt16, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public ushort? ReadUInt16()
{
try
{
Discard();
return _source.ReadUInt16();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt32, if possible
/// </summary>
/// <returns>The next UInt32, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public uint? ReadUInt32()
{
try
{
Discard();
return _source.ReadUInt32();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt64, if possible
/// </summary>
/// <returns>The next UInt64, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public ulong? ReadUInt64()
{
try
{
Discard();
return _source.ReadUInt64();
}
catch
{
return null;
}
}
/// <summary>
/// Read <paramref name="bytes"/> bytes, if possible
/// </summary>
/// <param name="bytes">Number of bytes to read</param>
/// <returns>The next <paramref name="bytes"/> bytes, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public byte[]? ReadBytes(int bytes)
{
try
{
Discard();
return _source.ReadBytes(bytes);
}
catch
{
return null;
}
}
/// <summary>
/// Read a single byte from the underlying stream, if possible
/// </summary>
/// <returns>The next full byte from the stream, null on error or end of stream</returns>
private byte? ReadSourceByte()
{
try
{
return _source.ReadByteValue();
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Read-only stream wrapper around multiple, consecutive streams
/// </summary>
public class ReadOnlyCompositeStream : Stream
{
#region Properties
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanSeek => true;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => _length;
/// <inheritdoc/>
public override long Position
{
get => _position;
set
{
_position = value;
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
}
}
#endregion
#region Internal State
/// <summary>
/// Internal collection of streams to read from
/// </summary>
private readonly List<Stream> _streams;
/// <summary>
/// Total length of all internal streams
/// </summary>
private long _length;
/// <summary>
/// Overall position in the stream wrapper
/// </summary>
private long _position;
#endregion
/// <summary>
/// Create a new, empty ReadOnlyCompositeStream
/// </summary>
public ReadOnlyCompositeStream()
{
_streams = [];
_length = 0;
_position = 0;
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from a single Stream
/// </summary>
/// <param name="stream"></param>
public ReadOnlyCompositeStream(Stream stream)
{
_streams = [stream];
_length = 0;
_position = 0;
// Verify the stream and add to the length
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"{nameof(stream)} needs to be readable and seekable");
_length += stream.Length;
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(Stream[] streams)
{
_streams = [.. streams];
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
{
_streams = new List<Stream>(streams);
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Add a new stream to the collection
/// </summary>
public bool AddStream(Stream stream)
{
// Verify the stream
if (!stream.CanRead || !stream.CanSeek)
return false;
// Add the stream to the end
_streams.Add(stream);
_length += stream.Length;
return true;
}
#region Stream Implementations
/// <inheritdoc/>
public override void Flush() => throw new NotImplementedException();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Determine which stream we start reading from
int streamIndex = DetermineStreamIndex(_position, out long streamOffset);
if (streamIndex == -1)
return 0;
// Determine if the stream fully contains the requested segment
bool singleStream = StreamContains(streamIndex, streamOffset, count);
// If we can read from a single stream
if (singleStream)
{
_position += count;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
return _streams[streamIndex].Read(buffer, offset, count);
}
// For all other cases, we read until there's no more
int readBytes = 0, originalCount = count;
while (readBytes < originalCount)
{
// Determine how much can be read from the current stream
long currentBytes = _streams[streamIndex].Length - streamOffset;
int shouldRead = Math.Min((int)currentBytes, count);
// Read from the current stream
_position += shouldRead;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
readBytes += _streams[streamIndex].Read(buffer, offset, shouldRead);
// Update the read variables
offset += shouldRead;
count -= shouldRead;
// Move to the next stream
streamIndex++;
streamOffset = 0;
// Validate the next stream exists
if (streamIndex >= _streams.Count)
break;
}
// Return the number of bytes that could be read
return readBytes;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
// Handle the "seek"
switch (origin)
{
case SeekOrigin.Begin: _position = offset; break;
case SeekOrigin.Current: _position += offset; break;
case SeekOrigin.End: _position = _length - offset - 1; break;
default: throw new ArgumentException($"Invalid value for {nameof(origin)}");
};
// Handle out-of-bounds seeks
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
return _position;
}
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotImplementedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
#endregion
#region Helpers
/// <summary>
/// Determine the index of the stream that contains a particular offset
/// </summary>
/// <param name="realOffset">Output parameter representing the real offset in the stream, -1 on error</param>
/// <returns>Index of the stream containing the offset, -1 on error</returns>
private int DetermineStreamIndex(long offset, out long realOffset)
{
// If the offset is out of bounds
if (offset < 0 || offset >= _length)
{
realOffset = -1;
return -1;
}
// Seek through until we hit the correct offset
long currentLength = 0;
for (int i = 0; i < _streams.Count; i++)
{
currentLength += _streams[i].Length;
if (currentLength > offset)
{
realOffset = offset - (currentLength - _streams[i].Length);
return i;
}
}
// Should never happen
realOffset = -1;
return -1;
}
/// <summary>
/// Determines if a stream contains a particular segment
/// </summary>
private bool StreamContains(int streamIndex, long offset, int length)
{
// Ensure the arguments are valid
if (streamIndex < 0 || streamIndex >= _streams.Count)
throw new ArgumentOutOfRangeException(nameof(streamIndex));
if (offset < 0 || offset >= _streams[streamIndex].Length)
throw new ArgumentOutOfRangeException(nameof(offset));
// Handle the general case
return _streams[streamIndex].Length - offset >= length;
}
#endregion
}
}

View File

@@ -118,7 +118,11 @@ namespace SabreTools.IO.Writers
/// </summary>
public ClrMameProWriter(Stream stream, Encoding encoding)
{
#if NET20 || NET35 || NET40
sw = new StreamWriter(stream, encoding);
#else
sw = new StreamWriter(stream, encoding, 1024, leaveOpen: true);
#endif
Quotes = true;
// Element stack
@@ -167,42 +171,6 @@ namespace SabreTools.IO.Writers
InternalWriteEndElement(true);
}
/// <summary>
/// Write a complete element with content
/// </summary>
public void WriteElementString(string name, string? value)
{
WriteStartElement(name);
WriteString(value);
WriteEndElement();
}
/// <summary>
/// Ensure writing writing null values as empty strings
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="value">Value to write in the element</param>
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
public void WriteRequiredElementString(string name, string? value, bool throwOnError = false)
{
// Throw an exception if we are configured to
if (value == null && throwOnError)
throw new ArgumentNullException(nameof(value));
WriteElementString(name, value ?? string.Empty);
}
/// <summary>
/// Write an element, if the value is not null or empty
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="value">Value to write in the element</param>
public void WriteOptionalElementString(string name, string? value)
{
if (!string.IsNullOrEmpty(value))
WriteElementString(name, value);
}
/// <summary>
/// Write the start of an attribute node
/// </summary>

View File

@@ -24,7 +24,11 @@ namespace SabreTools.IO.Writers
/// </summary>
public IniWriter(Stream stream, Encoding encoding)
{
#if NET20 || NET35 || NET40
sw = new StreamWriter(stream, encoding);
#else
sw = new StreamWriter(stream, encoding, 1024, leaveOpen: true);
#endif
}
/// <summary>

View File

@@ -54,7 +54,11 @@ namespace SabreTools.IO.Writers
/// </summary>
public SeparatedValueWriter(Stream stream, Encoding encoding)
{
#if NET20 || NET35 || NET40
sw = new StreamWriter(stream, encoding);
#else
sw = new StreamWriter(stream, encoding, 1024, leaveOpen: true);
#endif
}
/// <summary>
@@ -141,14 +145,6 @@ namespace SabreTools.IO.Writers
sw.Write(value);
}
/// <summary>
/// Write a newline
/// </summary>
public void WriteLine()
{
sw.WriteLine();
}
/// <summary>
/// Flush the underlying writer
/// </summary>

36
publish-nix.sh Executable file
View File

@@ -0,0 +1,36 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
NO_BUILD=false
while getopts "b" OPTION
do
case $OPTION in
b)
NO_BUILD=true
;;
*)
echo "Invalid option provided"
exit 1
;;
esac
done
# Set the current directory as a variable
BUILD_FOLDER=$PWD
# Only build if requested
if [ $NO_BUILD = false ]
then
# Restore Nuget packages for all builds
echo "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.IO/SabreTools.IO.csproj --output $BUILD_FOLDER
fi

26
publish-win.ps1 Normal file
View File

@@ -0,0 +1,26 @@
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
param(
[Parameter(Mandatory = $false)]
[Alias("NoBuild")]
[switch]$NO_BUILD
)
# Set the current directory as a variable
$BUILD_FOLDER = $PSScriptRoot
# Only build if requested
if (!$NO_BUILD.IsPresent)
{
# Restore Nuget packages for all builds
Write-Host "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.IO\SabreTools.IO.csproj --output $BUILD_FOLDER
}