├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── codeql.yml
│ └── dotnet-desktop.yml
├── .gitignore
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── Directory.csproj.props
├── Directory.csproj.targets
├── License.txt
├── OpenMcdf.Benchmarks
├── FileStreamRead.cs
├── FileStreamTransactedWrite.cs
├── FileStreamWrite.cs
├── MemoryStreamRead.cs
├── MemoryStreamTransactedWrite.cs
├── MemoryStreamWrite.cs
├── OpenMcdf.Benchmarks.csproj
├── OpenMcdfBenchmarks.cs
├── Program.cs
└── StructuredStorageBenchmarks.cs
├── OpenMcdf.CrossPlatform.slnf
├── OpenMcdf.Ole.Tests
├── 2custom.doc
├── CLSIDPropertyTest.file
├── Issue134.cfs
├── OlePropertiesExtensionsTests.cs
├── OpenMcdf.Ole.Tests.csproj
├── SampleWorkBook_bug98.xls
├── _Test.ppt
├── english.presets.doc
├── report.xls
├── winUnicodeDictionary.doc
└── wstr_presets.doc
├── OpenMcdf.Ole
├── CodePages.cs
├── DefaultPropertyFactory.cs
├── DictionaryProperty.cs
├── DocumentSummaryInfoPropertyFactory.cs
├── FormatIdentifiers.cs
├── IBinarySerializable.cs
├── IProperty.cs
├── ITypedPropertyValue.cs
├── OlePropertiesContainer.cs
├── OleProperty.cs
├── OpenMcdf.Ole.csproj
├── PropertyContext.cs
├── PropertyFactory.cs
├── PropertyIdentifierAndOffset.cs
├── PropertyIdentifiers.cs
├── PropertySet.cs
├── PropertySetNames.cs
├── PropertySetStream.cs
├── SpecialPropertyIdentifiers.cs
├── TypedPropertyValue.cs
└── VTPropertyType.cs
├── OpenMcdf.Perf
├── OpenMcdf.Perf.csproj
└── Program.cs
├── OpenMcdf.Tests
├── AssemblyInfo.cs
├── BinaryReaderTests.cs
├── BinaryWriterTests.cs
├── DebugWriter.cs
├── DirectoryEntryTests.cs
├── EntryInfoTests.cs
├── FatChainLoop_v3.cfs
├── MultipleStorage.cfs
├── MultipleStorage2.cfs
├── MultipleStorage3.cfs
├── MultipleStorage4.cfs
├── OpenMcdf.Tests.csproj
├── RootStorageTests.cs
├── StorageTests.cs
├── StreamAssert.cs
├── StreamExtensions.cs
├── StreamTests.cs
├── TestData.cs
├── TestStream_v3_0.cfs
├── TestStream_v3_4095.cfs
├── TestStream_v3_4096.cfs
├── TestStream_v3_4097.cfs
├── TestStream_v3_511.cfs
├── TestStream_v3_512.cfs
├── TestStream_v3_513.cfs
├── TestStream_v3_63.cfs
├── TestStream_v3_64.cfs
├── TestStream_v3_65.cfs
├── TestStream_v3_65536.cfs
├── TestStream_v4_0.cfs
├── TestStream_v4_4095.cfs
├── TestStream_v4_4096.cfs
├── TestStream_v4_4097.cfs
├── TestStream_v4_511.cfs
├── TestStream_v4_512.cfs
├── TestStream_v4_513.cfs
├── TestStream_v4_63.cfs
├── TestStream_v4_64.cfs
└── TestStream_v4_65.cfs
├── OpenMcdf.sln
├── OpenMcdf
├── AssemblyInfo.cs
├── CfbBinaryReader.cs
├── CfbBinaryWriter.cs
├── CfbStream.cs
├── ContextBase.cs
├── DifatSectorEnumerator.cs
├── DirectoryEntries.cs
├── DirectoryEntry.cs
├── DirectoryEntryComparer.cs
├── DirectoryEntryEnumerator.cs
├── DirectoryTree.cs
├── DirectoryTreeEnumerator.cs
├── EntryInfo.cs
├── Fat.cs
├── FatChainEnumerator.cs
├── FatEntry.cs
├── FatEnumerator.cs
├── FatSectorEnumerator.cs
├── FatStream.cs
├── FileFormatException.cs
├── Header.cs
├── MiniFat.cs
├── MiniFatChainEnumerator.cs
├── MiniFatEnumerator.cs
├── MiniFatStream.cs
├── MiniSector.cs
├── OpenMcdf.csproj
├── RootContext.cs
├── RootContextSite.cs
├── RootStorage.cs
├── Sector.cs
├── SectorDataCache.cs
├── SectorType.cs
├── Storage.cs
├── StreamExtensions.cs
├── System
│ ├── .editorconfig
│ ├── CompilerServices.cs
│ ├── LICENSE.TXT
│ └── NullableAttributes.cs
├── ThrowHelper.cs
└── TransactedStream.cs
├── README.md
├── ReleaseNotes.md
├── StructuredStorage
├── LockBytes.cs
├── NativeMethods.json
├── NativeMethods.txt
├── PropertySetStorage.cs
├── PropertyStorage.cs
├── Storage.cs
├── Stream.cs
└── StructuredStorage.csproj
├── StructuredStorageExplorer
├── MainForm.Designer.cs
├── MainForm.cs
├── MainForm.resx
├── OpenMcdf.snk
├── PreferencesForm.Designer.cs
├── PreferencesForm.cs
├── PreferencesForm.resx
├── Program.cs
├── Properties
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── StreamDataProvider.cs
├── StructuredStorageExplorer.csproj
├── Utils.cs
├── app.config
└── img
│ ├── disk.png
│ ├── door_out.png
│ ├── folder.png
│ ├── page_white.png
│ ├── storage.png
│ └── stream.png
├── docs
├── img
│ ├── LOGO_OpenMcdf.png
│ ├── LOGO_OpenMcdf_H.png
│ ├── PartialStream.PNG
│ ├── read_stream.PNG
│ ├── structured_storage.png
│ ├── visit_entries.PNG
│ └── write_stream.PNG
├── index.htm
└── main.css
├── exclusion.dic
├── global.json
└── icon.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL Advanced"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | branches: [ "master" ]
19 | schedule:
20 | - cron: '18 14 * * 6'
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze (${{ matrix.language }})
25 | # Runner size impacts CodeQL analysis time. To learn more, please see:
26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
27 | # - https://gh.io/supported-runners-and-hardware-resources
28 | # - https://gh.io/using-larger-runners (GitHub.com only)
29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31 | permissions:
32 | # required for all workflows
33 | security-events: write
34 |
35 | # required to fetch internal or private CodeQL packs
36 | packages: read
37 |
38 | # only required for workflows in private repositories
39 | actions: read
40 | contents: read
41 |
42 | strategy:
43 | fail-fast: false
44 | matrix:
45 | include:
46 | - language: csharp
47 | build-mode: none
48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
49 | # Use `c-cpp` to analyze code written in C, C++ or both
50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
56 | steps:
57 | - name: Checkout repository
58 | uses: actions/checkout@v4
59 |
60 | # Initializes the CodeQL tools for scanning.
61 | - name: Initialize CodeQL
62 | uses: github/codeql-action/init@v3
63 | with:
64 | languages: ${{ matrix.language }}
65 | build-mode: ${{ matrix.build-mode }}
66 | # If you wish to specify custom queries, you can do so here or in a config file.
67 | # By default, queries listed here will override any specified in a config file.
68 | # Prefix the list here with "+" to use these queries and those in the config file.
69 |
70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
71 | # queries: security-extended,security-and-quality
72 |
73 | # If the analyze step fails for one of the languages you are analyzing with
74 | # "We were unable to automatically build your code", modify the matrix above
75 | # to set the build mode to "manual" for that language. Then modify this step
76 | # to build your code.
77 | # ℹ️ Command-line programs to run using the OS shell.
78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
79 | - if: matrix.build-mode == 'manual'
80 | shell: bash
81 | run: |
82 | echo 'If you are using a "manual" build mode for one or more of the' \
83 | 'languages you are analyzing, replace this with the commands to build' \
84 | 'your code, for example:'
85 | echo ' make bootstrap'
86 | echo ' make release'
87 | exit 1
88 |
89 | - name: Perform CodeQL Analysis
90 | uses: github/codeql-action/analyze@v3
91 | with:
92 | category: "/language:${{matrix.language}}"
93 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet-desktop.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core Desktop
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 |
11 | build:
12 |
13 | strategy:
14 | matrix:
15 | configuration: [Debug, Release]
16 |
17 | runs-on: windows-latest # For a list of available runner types, refer to
18 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
19 |
20 | env:
21 | Solution_Name: OpenMcdf.sln
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | # Install the .NET Core workload
30 | - name: Install .NET Core
31 | uses: actions/setup-dotnet@v4
32 | with:
33 | dotnet-version: 8.0.x
34 |
35 | # Check formatting
36 | - name: Check formatting
37 | run: dotnet format --verify-no-changes $env:Solution_Name
38 |
39 | # Execute all unit tests in the solution
40 | - name: Execute unit tests
41 | run: dotnet test --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" $env:Solution_Name
42 |
43 | # Restore the application to populate the obj folder with RuntimeIdentifiers
44 | - name: Restore the application
45 | run: dotnet build -c $env:Configuration /p:ContinuousIntegrationBuild=true $env:Solution_Name
46 | env:
47 | Configuration: ${{ matrix.configuration }}
48 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | true
7 |
8 |
--------------------------------------------------------------------------------
/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 | $(NoWarn);NU1507
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Directory.csproj.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 12
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 | 3.0.0.0
14 | ironfede,jeremy-visionaid
15 | Copyright © 2010-2025, Federico Blaseotto, Jeremy Powell
16 | https://github.com/ironfede/openmcdf
17 | icon.png
18 | README.md
19 | https://github.com/ironfede/openmcdf
20 | git
21 |
22 | MPL-2.0
23 | True
24 | True
25 | snupkg
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | latest
36 | true
37 |
38 |
39 | true
40 |
41 |
--------------------------------------------------------------------------------
/Directory.csproj.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/FileStreamRead.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [MediumRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class FileStreamRead : IDisposable
12 | {
13 | private string readFileName = "";
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | File.Delete(readFileName);
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | readFileName = Path.GetTempFileName();
34 |
35 | buffer = new byte[BufferSize];
36 |
37 | using var storage = RootStorage.Create(readFileName, Version);
38 | using CfbStream stream = storage.CreateStream("Test");
39 |
40 | int iterationCount = StreamLength / BufferSize;
41 | for (int iteration = 0; iteration < iterationCount; ++iteration)
42 | stream.Write(buffer);
43 | }
44 |
45 | [GlobalCleanup]
46 | public void GlobalCleanup() => Dispose();
47 |
48 | [Benchmark]
49 | public void Read() => OpenMcdfBenchmarks.ReadStream(readFileName!, buffer);
50 |
51 | #if WINDOWS
52 | [Benchmark(Baseline = true)]
53 | public void ReadStructuredStorage() => StructuredStorageBenchmarks.ReadStream(readFileName!, buffer);
54 | #endif
55 | }
56 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/FileStreamTransactedWrite.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [MediumRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class FileStreamTransactedWrite : IDisposable
12 | {
13 | private string writeFileName = "";
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | File.Delete(writeFileName);
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | writeFileName = Path.GetTempFileName();
34 |
35 | buffer = new byte[BufferSize];
36 | }
37 |
38 | [GlobalCleanup]
39 | public void GlobalCleanup() => Dispose();
40 |
41 | [Benchmark]
42 | public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeFileName!, Version, StorageModeFlags.None | StorageModeFlags.Transacted, buffer, StreamLength);
43 |
44 | #if WINDOWS
45 |
46 | [Benchmark(Baseline = true)]
47 | public void WriteStructuredStorageTransacted() => StructuredStorageBenchmarks.WriteStream(writeFileName, Version, StorageModeFlags.Transacted, buffer, StreamLength);
48 | #endif
49 | }
50 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/FileStreamWrite.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [MediumRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class FileStreamWrite : IDisposable
12 | {
13 | private string writeFileName = "";
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | File.Delete(writeFileName);
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | writeFileName = Path.GetTempFileName();
34 |
35 | buffer = new byte[BufferSize];
36 | }
37 |
38 | [GlobalCleanup]
39 | public void GlobalCleanup() => Dispose();
40 |
41 | [Benchmark]
42 | public void Write() => OpenMcdfBenchmarks.WriteStream(writeFileName!, Version, StorageModeFlags.None, buffer, StreamLength);
43 |
44 | #if WINDOWS
45 | [Benchmark(Baseline = true)]
46 | public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteStream(writeFileName, Version, StorageModeFlags.None, buffer, StreamLength);
47 | #endif
48 | }
49 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/MemoryStreamRead.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [ShortRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class MemoryStreamRead : IDisposable
12 | {
13 | private MemoryStream? readStream;
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | readStream?.Dispose();
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | buffer = new byte[BufferSize];
34 | readStream = new MemoryStream(2 * StreamLength);
35 |
36 | using var storage = RootStorage.Create(readStream, Version, StorageModeFlags.LeaveOpen);
37 | using CfbStream stream = storage.CreateStream("Test");
38 |
39 | int iterationCount = StreamLength / BufferSize;
40 | for (int iteration = 0; iteration < iterationCount; ++iteration)
41 | stream.Write(buffer);
42 | }
43 |
44 | [GlobalCleanup]
45 | public void GlobalCleanup() => Dispose();
46 |
47 | [Benchmark]
48 | public void Read() => OpenMcdfBenchmarks.ReadStream(readStream!, buffer);
49 |
50 | #if WINDOWS
51 | [Benchmark(Baseline = true)]
52 | public void ReadStructuredStorage() => StructuredStorageBenchmarks.ReadStream(readStream!, buffer);
53 | #endif
54 | }
55 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/MemoryStreamTransactedWrite.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [ShortRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class MemoryStreamTransactedWrite : IDisposable
12 | {
13 | private MemoryStream? writeStream;
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | writeStream?.Dispose();
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | buffer = new byte[BufferSize];
34 | writeStream = new MemoryStream(2 * StreamLength);
35 | }
36 |
37 | [GlobalCleanup]
38 | public void GlobalCleanup() => Dispose();
39 |
40 | [Benchmark]
41 | public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, Version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength);
42 | }
43 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/MemoryStreamWrite.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Columns;
3 | using OpenMcdf.Benchmarks;
4 |
5 | namespace OpenMcdf.Benchmark;
6 |
7 | [ShortRunJob]
8 | [MemoryDiagnoser]
9 | [HideColumns(Column.AllocRatio)]
10 | [MarkdownExporter]
11 | public class MemoryStreamWrite : IDisposable
12 | {
13 | private MemoryStream? writeStream;
14 | private byte[] buffer = Array.Empty();
15 |
16 | [Params(Version.V3, Version.V4)]
17 | public Version Version { get; set; }
18 |
19 | [Params(512, 1024 * 1024)]
20 | public int BufferSize { get; set; }
21 |
22 | [Params(1024 * 1024)]
23 | public int StreamLength { get; set; }
24 |
25 | public void Dispose()
26 | {
27 | writeStream?.Dispose();
28 | }
29 |
30 | [GlobalSetup]
31 | public void GlobalSetup()
32 | {
33 | buffer = new byte[BufferSize];
34 | writeStream = new MemoryStream(2 * StreamLength);
35 | }
36 |
37 | [GlobalCleanup]
38 | public void GlobalCleanup() => Dispose();
39 |
40 | [Benchmark]
41 | public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, Version, StorageModeFlags.LeaveOpen, buffer, StreamLength);
42 |
43 | #if WINDOWS
44 | [Benchmark(Baseline = true)]
45 | public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(Version, StorageModeFlags.LeaveOpen, buffer, StreamLength);
46 | #endif
47 | }
48 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | net8.0-windows
6 | Exe
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/OpenMcdfBenchmarks.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Benchmarks;
2 |
3 | internal static class OpenMcdfBenchmarks
4 | {
5 | public static void ReadStream(string fileName, byte[] buffer)
6 | {
7 | using FileStream fileStream = File.OpenRead(fileName);
8 | ReadStream(fileStream, buffer);
9 | }
10 |
11 | public static void ReadStream(Stream stream, byte[] buffer)
12 | {
13 | using var storage = RootStorage.Open(stream, StorageModeFlags.LeaveOpen);
14 | using CfbStream cfbStream = storage.OpenStream("Test");
15 | while (cfbStream.Position < cfbStream.Length)
16 | {
17 | int read = cfbStream.Read(buffer, 0, buffer.Length);
18 | if (read <= 0)
19 | throw new EndOfStreamException();
20 | }
21 | }
22 |
23 | public static void WriteStream(string fileName, Version version, StorageModeFlags flags, byte[] buffer, long streamLength)
24 | {
25 | using FileStream fileStream = File.Create(fileName);
26 | WriteStream(fileStream, version, flags, buffer, streamLength);
27 | }
28 |
29 | public static void WriteStream(Stream stream, Version version, StorageModeFlags flags, byte[] buffer, long streamLength)
30 | {
31 | using var storage = RootStorage.Create(stream, version, flags);
32 | using CfbStream cfbStream = storage.CreateStream("Test");
33 |
34 | while (stream.Length < streamLength)
35 | stream.Write(buffer, 0, buffer.Length);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Running;
2 |
3 | namespace OpenMcdf.Benchmarks;
4 |
5 | internal sealed class Program
6 | {
7 | private static void Main(string[] args)
8 | {
9 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/OpenMcdf.Benchmarks/StructuredStorageBenchmarks.cs:
--------------------------------------------------------------------------------
1 | using StructuredStorage;
2 |
3 | namespace OpenMcdf.Benchmarks;
4 |
5 | internal static class StructuredStorageBenchmarks
6 | {
7 | public static void ReadStream(string fileName, byte[] buffer)
8 | {
9 | using var storage = StructuredStorage.Storage.Open(fileName);
10 | using StructuredStorage.Stream storageStream = storage.OpenStream("Test");
11 |
12 | long length = storageStream.Length; // Length lookup is expensive
13 | long totalRead = 0;
14 | while (totalRead < length)
15 | {
16 | int read = storageStream.Read(buffer, 0, buffer.Length);
17 | if (read <= 0)
18 | throw new EndOfStreamException($"Read past end of stream at {storageStream.Position}/{storageStream.Length}");
19 | totalRead += read;
20 | }
21 | }
22 |
23 | public static void ReadStream(MemoryStream stream, byte[] buffer)
24 | {
25 | using var storage = StructuredStorage.Storage.Open(stream);
26 | using StructuredStorage.Stream storageStream = storage.OpenStream("Test");
27 |
28 | long length = storageStream.Length; // Length lookup is expensive
29 | long totalRead = 0;
30 | while (totalRead < length)
31 | {
32 | int read = storageStream.Read(buffer, 0, buffer.Length);
33 | if (read <= 0)
34 | throw new EndOfStreamException($"Read past end of stream at {storageStream.Position}/{storageStream.Length}");
35 | totalRead += read;
36 | }
37 | }
38 |
39 | public static void WriteStream(string fileName, Version version, StorageModeFlags flags, byte[] buffer, long streamLength)
40 | {
41 | File.Delete(fileName);
42 |
43 | StorageModes modes = StorageModes.AccessReadWrite | StorageModes.ShareExclusive;
44 | if (flags.HasFlag(StorageModeFlags.Transacted))
45 | modes |= StorageModes.Transacted;
46 |
47 | using var storage = StructuredStorage.Storage.Create(fileName, modes, version == Version.V4);
48 | using StructuredStorage.Stream storageStream = storage.CreateStream("Test");
49 |
50 | long totalWritten = 0;
51 | while (totalWritten < streamLength)
52 | {
53 | storageStream.Write(buffer, 0, buffer.Length);
54 | totalWritten += buffer.Length;
55 | }
56 | }
57 |
58 | public static void WriteInMemory(Version version, StorageModeFlags flags, byte[] buffer, long streamLength)
59 | {
60 | if (version == Version.V4)
61 | throw new NotSupportedException();
62 |
63 | using var storage = StructuredStorage.Storage.CreateInMemory((int)streamLength * 2);
64 | using StructuredStorage.Stream storageStream = storage.CreateStream("Test");
65 |
66 | long totalWritten = 0;
67 | while (totalWritten < streamLength)
68 | {
69 | storageStream.Write(buffer, 0, buffer.Length);
70 | totalWritten += buffer.Length;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/OpenMcdf.CrossPlatform.slnf:
--------------------------------------------------------------------------------
1 | {
2 | "solution": {
3 | "path": "OpenMcdf.sln",
4 | "projects": [
5 | "OpenMcdf/OpenMcdf.csproj",
6 | "OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj",
7 | "OpenMcdf.Ole/OpenMcdf.Ole.csproj",
8 | "OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj",
9 | "OpenMcdf.Perf/OpenMcdf.Perf.csproj",
10 | "OpenMcdf.Tests/OpenMcdf.Tests.csproj"
11 | ]
12 | }
13 | }
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/2custom.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/2custom.doc
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/CLSIDPropertyTest.file:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/CLSIDPropertyTest.file
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/Issue134.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/Issue134.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;net8.0
5 | Exe
6 |
7 | false
8 | true
9 | true
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | PreserveNewest
28 |
29 |
30 | PreserveNewest
31 |
32 |
33 | PreserveNewest
34 |
35 |
36 | Always
37 |
38 |
39 | PreserveNewest
40 |
41 |
42 | PreserveNewest
43 |
44 |
45 | PreserveNewest
46 |
47 |
48 | Always
49 |
50 |
51 | PreserveNewest
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/_Test.ppt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/_Test.ppt
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/english.presets.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/english.presets.doc
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/report.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/report.xls
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/winUnicodeDictionary.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/winUnicodeDictionary.doc
--------------------------------------------------------------------------------
/OpenMcdf.Ole.Tests/wstr_presets.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Ole.Tests/wstr_presets.doc
--------------------------------------------------------------------------------
/OpenMcdf.Ole/CodePages.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal static class CodePages
4 | {
5 | public const int WinUnicode = 0x04B0;
6 | }
7 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/DefaultPropertyFactory.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | // The default property factory.
4 | internal sealed class DefaultPropertyFactory : PropertyFactory
5 | {
6 | public static DefaultPropertyFactory Default { get; } = new();
7 | }
8 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/DictionaryProperty.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace OpenMcdf.Ole;
4 |
5 | internal sealed class DictionaryProperty : IProperty
6 | {
7 | private readonly int codePage;
8 | private Dictionary? entries = new();
9 |
10 | public DictionaryProperty(int codePage)
11 | {
12 | this.codePage = codePage;
13 | }
14 |
15 | public PropertyType PropertyType => PropertyType.DictionaryProperty;
16 |
17 | public object? Value
18 | {
19 | get => entries;
20 | set => entries = (Dictionary?)value;
21 | }
22 |
23 | public void Read(BinaryReader br)
24 | {
25 | long curPos = br.BaseStream.Position;
26 |
27 | uint numEntries = br.ReadUInt32();
28 |
29 | for (uint i = 0; i < numEntries; i++)
30 | {
31 | ReadEntry(br);
32 | }
33 |
34 | int m = (int)(br.BaseStream.Position - curPos) % 4;
35 |
36 | if (m > 0)
37 | {
38 | for (int i = 0; i < m; i++)
39 | {
40 | br.ReadByte();
41 | }
42 | }
43 | }
44 |
45 | // Read a single dictionary entry
46 | private void ReadEntry(BinaryReader br)
47 | {
48 | uint propertyIdentifier = br.ReadUInt32();
49 | int length = br.ReadInt32();
50 |
51 | byte[] nameBytes;
52 |
53 | if (codePage == CodePages.WinUnicode)
54 | {
55 | nameBytes = br.ReadBytes(length << 1);
56 |
57 | int m = length * 2 % 4;
58 | if (m > 0)
59 | br.ReadBytes(m);
60 | }
61 | else
62 | {
63 | nameBytes = br.ReadBytes(length);
64 | }
65 |
66 | string entryName = Encoding.GetEncoding(codePage).GetString(nameBytes).Trim('\0');
67 | entries!.Add(propertyIdentifier, entryName);
68 | }
69 |
70 | ///
71 | /// Write the dictionary and all its values into the specified .
72 | ///
73 | ///
74 | /// Based on the Microsoft specifications at https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/99127b7f-c440-4697-91a4-c853086d6b33
75 | ///
76 | /// A writer to write the dictionary into.
77 | public void Write(BinaryWriter bw)
78 | {
79 | long curPos = bw.BaseStream.Position;
80 |
81 | bw.Write(entries!.Count);
82 |
83 | foreach (KeyValuePair kv in entries)
84 | {
85 | WriteEntry(bw, kv.Key, kv.Value);
86 | }
87 |
88 | int size = (int)(bw.BaseStream.Position - curPos);
89 | WritePaddingIfNeeded(bw, size);
90 | }
91 |
92 | // Write a single entry to the dictionary, and handle and required null termination and padding.
93 | private void WriteEntry(BinaryWriter bw, uint propertyIdentifier, string name)
94 | {
95 | // Write the PropertyIdentifier
96 | bw.Write(propertyIdentifier);
97 |
98 | // Encode string data with the current code page
99 | byte[] nameBytes = Encoding.GetEncoding(codePage).GetBytes(name);
100 | uint byteLength = (uint)nameBytes.Length;
101 |
102 | // If the code page is WINUNICODE, write the length as the number of characters and pad the length to a multiple of 4 bytes
103 | // Otherwise, write the length as the number of bytes and don't pad.
104 | // In either case, the string must be null terminated
105 | if (codePage == CodePages.WinUnicode)
106 | {
107 | // Two bytes padding for Unicode strings
108 | byteLength += 2;
109 |
110 | bw.Write(byteLength / 2);
111 | bw.Write(nameBytes);
112 | bw.Write((byte)0);
113 | bw.Write((byte)0);
114 |
115 | WritePaddingIfNeeded(bw, (int)byteLength);
116 | }
117 | else
118 | {
119 | byteLength += 1;
120 |
121 | bw.Write(byteLength);
122 | bw.Write(nameBytes);
123 | bw.Write((byte)0);
124 | }
125 | }
126 |
127 | // Write as much padding as needed to pad fieldLength to a multiple of 4 bytes
128 | private static void WritePaddingIfNeeded(BinaryWriter bw, int fieldLength)
129 | {
130 | int m = fieldLength % 4;
131 |
132 | if (m > 0)
133 | {
134 | for (int i = 0; i < 4 - m; i++) // padding
135 | bw.Write((byte)0);
136 | }
137 | }
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | // A separate factory for DocumentSummaryInformation properties, to handle special cases with unaligned strings.
4 | internal sealed class DocumentSummaryInfoPropertyFactory : PropertyFactory
5 | {
6 | public static DocumentSummaryInfoPropertyFactory Default { get; } = new();
7 |
8 | protected override ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant)
9 | {
10 | // PIDDSI_HEADINGPAIR and PIDDSI_DOCPARTS use unaligned (unpadded) strings - the others are padded
11 | if (propertyIdentifier is 0x0000000C or 0x0000000D)
12 | return new VT_Unaligned_LPSTR_Property(vType, codePage, isVariant);
13 |
14 | return base.CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/FormatIdentifiers.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | public static class FormatIdentifiers
4 | {
5 | public static readonly Guid SummaryInformation = new("{F29F85E0-4FF9-1068-AB91-08002B27B3D9}");
6 | public static readonly Guid DocSummaryInformation = new("{D5CDD502-2E9C-101B-9397-08002B2CF9AE}");
7 | public static readonly Guid UserDefinedProperties = new("{D5CDD505-2E9C-101B-9397-08002B2CF9AE}");
8 | public static readonly Guid GlobalInfo = new("{56616F00-C154-11CE-8553-00AA00A1F95B}");
9 | public static readonly Guid ImageContents = new("{56616400-C154-11CE-8553-00AA00A1F95B}");
10 | public static readonly Guid ImageInfo = new("{56616500-C154-11CE-8553-00AA00A1F95B}");
11 | }
12 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/IBinarySerializable.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal interface IBinarySerializable
4 | {
5 | void Write(BinaryWriter bw);
6 | void Read(BinaryReader br);
7 | }
8 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/IProperty.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal enum PropertyType
4 | {
5 | TypedPropertyValue = 0,
6 | DictionaryProperty = 1
7 | }
8 |
9 | internal interface IProperty : IBinarySerializable
10 | {
11 | object? Value { get; set; }
12 |
13 | PropertyType PropertyType { get; }
14 | }
15 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/ITypedPropertyValue.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal enum PropertyDimensions
4 | {
5 | IsScalar,
6 | IsVector,
7 | IsArray
8 | }
9 |
10 | internal interface ITypedPropertyValue : IProperty
11 | {
12 | VTPropertyType VTType { get; }
13 |
14 | PropertyDimensions PropertyDimensions { get; }
15 |
16 | bool IsVariant { get; }
17 | }
18 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/OleProperty.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | public sealed class OleProperty
4 | {
5 | private readonly OlePropertiesContainer container;
6 | object? value;
7 |
8 | internal OleProperty(OlePropertiesContainer container)
9 | {
10 | this.container = container;
11 | }
12 |
13 | public string PropertyName => PropertyIdentifiers.GetDescription(PropertyIdentifier, container.ContainerType, container.PropertyNames);
14 |
15 | public uint PropertyIdentifier { get; internal set; }
16 |
17 | public VTPropertyType VTType { get; internal set; }
18 |
19 | public object? Value
20 | {
21 | get
22 | {
23 | switch (VTType)
24 | {
25 | case VTPropertyType.VT_LPSTR:
26 | case VTPropertyType.VT_LPWSTR:
27 | if (value is string str && !string.IsNullOrEmpty(str))
28 | return str.Trim('\0');
29 | break;
30 | default:
31 | return value;
32 | }
33 |
34 | return value;
35 | }
36 | set => this.value = value;
37 | }
38 |
39 | public override bool Equals(object? obj) => obj is OleProperty other && other.PropertyIdentifier == PropertyIdentifier;
40 |
41 | public override int GetHashCode() => (int)PropertyIdentifier;
42 |
43 | public override string ToString() => $"{PropertyName} - {VTType} - {Value}";
44 | }
45 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/OpenMcdf.Ole.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;net8.0
5 | true
6 | true
7 |
8 |
9 |
10 | True
11 | OpenMcdf OLE
12 |
13 | OpenMcdf is a 100% .NET / C# component that allows developers to manipulate Microsoft Compound Document File Format for OLE structured storage.
14 | It supports read/write operations on streams and storages and traversal of directory trees.
15 |
16 | MS-CFB Compound File Binary File Format Structured Storage OLE Object Linking and Embedding Property Set Data Structures
17 | $(Version)-preview.6
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/PropertyContext.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | public enum Behavior
4 | {
5 | CaseSensitive,
6 | CaseInsensitive
7 | }
8 |
9 | public sealed class PropertyContext
10 | {
11 | public int CodePage { get; set; }
12 | public Behavior Behavior { get; set; }
13 | public uint Locale { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/PropertyIdentifierAndOffset.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal sealed class PropertyIdentifierAndOffset : IBinarySerializable
4 | {
5 | public uint PropertyIdentifier { get; set; }
6 | public uint Offset { get; set; }
7 |
8 | public void Read(BinaryReader br)
9 | {
10 | PropertyIdentifier = br.ReadUInt32();
11 | Offset = br.ReadUInt32();
12 | }
13 |
14 | public void Write(BinaryWriter bw)
15 | {
16 | bw.Write(PropertyIdentifier);
17 | bw.Write(Offset);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/PropertyIdentifiers.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Frozen;
2 |
3 | namespace OpenMcdf.Ole;
4 |
5 | public static class PropertyIdentifiers
6 | {
7 | readonly static FrozenDictionary SummaryInfo = new Dictionary()
8 | {
9 | {0x00000001, "PIDSI_CODEPAGE" },
10 | {0x00000002, "PIDSI_TITLE" },
11 | {0x00000003, "PIDSI_SUBJECT" },
12 | {0x00000004, "PIDSI_AUTHOR" },
13 | {0x00000005, "PIDSI_KEYWORDS" },
14 | {0x00000006, "PIDSI_COMMENTS" },
15 | {0x00000007, "PIDSI_TEMPLATE" },
16 | {0x00000008, "PIDSI_LASTAUTHOR" },
17 | {0x00000009, "PIDSI_REVNUMBER" },
18 | {0x0000000A, "PIDSI_EDITTIME" },
19 | {0x0000000B, "PIDSI_LASTPRINTED" },
20 | {0x0000000C, "PIDSI_CREATE_DTM" },
21 | {0x0000000D, "PIDSI_LASTSAVE_DTM" },
22 | {0x0000000E, "PIDSI_PAGECOUNT" },
23 | {0x0000000F, "PIDSI_WORDCOUNT" },
24 | {0x00000010, "PIDSI_CHARCOUNT" },
25 | {0x00000012, "PIDSI_APPNAME" },
26 | {0x00000013, "PIDSI_DOC_SECURITY" }
27 | }.ToFrozenDictionary();
28 |
29 | readonly static FrozenDictionary DocumentSummaryInfo = new Dictionary()
30 | {
31 | {0x00000001, "PIDDSI_CODEPAGE" },
32 | {0x00000002, "PIDDSI_CATEGORY" },
33 | {0x00000003, "PIDDSI_PRESFORMAT" },
34 | {0x00000004, "PIDDSI_BYTECOUNT" },
35 | {0x00000005, "PIDDSI_LINECOUNT" },
36 | {0x00000006, "PIDDSI_PARCOUNT" },
37 | {0x00000007, "PIDDSI_SLIDECOUNT" },
38 | {0x00000008, "PIDDSI_NOTECOUNT" },
39 | {0x00000009, "PIDDSI_HIDDENCOUNT" },
40 | {0x0000000A, "PIDDSI_MMCLIPCOUNT" },
41 | {0x0000000B, "PIDDSI_SCALE" },
42 | {0x0000000C, "PIDDSI_HEADINGPAIR" },
43 | {0x0000000D, "PIDDSI_DOCPARTS" },
44 | {0x0000000E, "PIDDSI_MANAGER" },
45 | {0x0000000F, "PIDDSI_COMPANY" },
46 | {0x00000010, "PIDDSI_LINKSDIRTY" },
47 | {0x00000011, "PIDDSI_CCHWITHSPACES" },
48 | {0x00000013, "PIDDSI_SHAREDDOC" },
49 | {0x00000014, "PIDDSI_LINKBASE" },
50 | {0x00000015, "PIDDSI_HLINKS" },
51 | {0x00000016, "PIDDSI_HYPERLINKSCHANGED" },
52 | {0x00000017, "PIDDSI_VERSION" },
53 | {0x00000018, "PIDDSI_DIGSIG" },
54 | {0x0000001A, "PIDDSI_CONTENTTYPE" },
55 | {0x0000001B, "PIDDSI_CONTENTSTATUS" },
56 | {0x0000001C, "PIDDSI_LANGUAGE" },
57 | {0x0000001D, "PIDDSI_DOCVERSION" }
58 | }.ToFrozenDictionary();
59 |
60 | public static string GetDescription(uint identifier, ContainerType map, IDictionary? customDictionary = null)
61 | {
62 | IDictionary nameDictionary;
63 |
64 | if (customDictionary is null)
65 | {
66 | nameDictionary = map switch
67 | {
68 | ContainerType.SummaryInfo => SummaryInfo,
69 | ContainerType.DocumentSummaryInfo => DocumentSummaryInfo,
70 | _ => throw new ArgumentException("Unknown container type", nameof(map)),
71 | };
72 | }
73 | else
74 | {
75 | nameDictionary = customDictionary;
76 | }
77 |
78 | if (nameDictionary.TryGetValue(identifier, out string? value))
79 | return value;
80 |
81 | return $"0x{identifier:x8}";
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/PropertySet.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal sealed class PropertySet
4 | {
5 | public PropertyContext PropertyContext { get; set; } = new();
6 |
7 | public uint Size { get; set; }
8 |
9 | public List PropertyIdentifierAndOffsets { get; } = new();
10 |
11 | public List Properties { get; } = new();
12 |
13 | public void LoadContext(int propertySetOffset, BinaryReader br)
14 | {
15 | long currPos = br.BaseStream.Position;
16 |
17 | // Read the code page - this should always be present
18 | int codePageOffset = (int)(propertySetOffset + PropertyIdentifierAndOffsets.First(pio => pio.PropertyIdentifier == SpecialPropertyIdentifiers.CodePage).Offset);
19 | br.BaseStream.Seek(codePageOffset, SeekOrigin.Begin);
20 |
21 | var vType = (VTPropertyType)br.ReadUInt16();
22 | br.ReadUInt16(); // Ushort Padding
23 | PropertyContext.CodePage = (ushort)br.ReadInt16();
24 |
25 | // Read the Locale, if present
26 | PropertyIdentifierAndOffset? localeProperty = PropertyIdentifierAndOffsets.FirstOrDefault(pio => pio.PropertyIdentifier == SpecialPropertyIdentifiers.Locale);
27 | if (localeProperty is not null)
28 | {
29 | long localeOffset = (propertySetOffset + localeProperty.Offset);
30 | br.BaseStream.Seek(localeOffset, SeekOrigin.Begin);
31 |
32 | vType = (VTPropertyType)br.ReadUInt16();
33 | br.ReadUInt16(); // Ushort Padding
34 | PropertyContext.Locale = br.ReadUInt32();
35 | }
36 |
37 | br.BaseStream.Position = currPos;
38 | }
39 |
40 | public void Add(IDictionary propertyNames)
41 | {
42 | DictionaryProperty dictionaryProperty = new(PropertyContext.CodePage)
43 | {
44 | Value = propertyNames
45 | };
46 | Properties.Add(dictionaryProperty);
47 | PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = SpecialPropertyIdentifiers.Dictionary, Offset = 0 });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/PropertySetNames.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 |
4 | ///
5 | /// Some well known property set names.
6 | ///
7 | ///
8 | /// As defined at https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/e5484a83-3cc1-43a6-afcf-6558059fe36e
9 | ///
10 | public static class PropertySetNames
11 | {
12 | public const string SummaryInformation = "\u0005SummaryInformation";
13 | public const string DocSummaryInformation = "\u0005DocumentSummaryInformation";
14 | public const string GlobalInfo = "\u0005GlobalInfo";
15 | public const string ImageContents = "\u000505ImageContents";
16 | public const string ImageInfo = "\u0005ImageInfo";
17 | }
18 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/SpecialPropertyIdentifiers.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | // Identifiers for properties that we treat specially.
4 | internal static class SpecialPropertyIdentifiers
5 | {
6 | public const uint Dictionary = 0;
7 | public const uint CodePage = 1;
8 | public const uint Locale = 0x80000000;
9 | }
10 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/TypedPropertyValue.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | internal abstract class TypedPropertyValue : ITypedPropertyValue
4 | {
5 | private readonly VTPropertyType _VTType;
6 | protected object? propertyValue;
7 |
8 | public PropertyType PropertyType => PropertyType.TypedPropertyValue;
9 |
10 | public VTPropertyType VTType => _VTType;
11 |
12 | public TypedPropertyValue(VTPropertyType vtType, bool isVariant = false)
13 | {
14 | _VTType = vtType;
15 | PropertyDimensions = CheckPropertyDimensions(vtType);
16 | IsVariant = isVariant;
17 | }
18 |
19 | public PropertyDimensions PropertyDimensions { get; } = PropertyDimensions.IsScalar;
20 |
21 | public bool IsVariant { get; }
22 |
23 | protected virtual bool NeedsPadding { get; set; } = true;
24 |
25 | private static PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType)
26 | {
27 | if ((((ushort)vtType) & 0x1000) != 0)
28 | return PropertyDimensions.IsVector;
29 | if ((((ushort)vtType) & 0x2000) != 0)
30 | return PropertyDimensions.IsArray;
31 | return PropertyDimensions.IsScalar;
32 | }
33 |
34 | public virtual object? Value
35 | {
36 | get => propertyValue;
37 | set => propertyValue = value;
38 | }
39 |
40 | public abstract T? ReadScalarValue(BinaryReader br);
41 |
42 | public void Read(BinaryReader br)
43 | {
44 | long currentPos = br.BaseStream.Position;
45 |
46 | switch (PropertyDimensions)
47 | {
48 | case PropertyDimensions.IsScalar:
49 | {
50 | propertyValue = ReadScalarValue(br);
51 | int size = (int)(br.BaseStream.Position - currentPos);
52 |
53 | int m = size % 4;
54 |
55 | if (m > 0 && NeedsPadding)
56 | br.ReadBytes(4 - m); // padding
57 | }
58 |
59 | break;
60 |
61 | case PropertyDimensions.IsVector:
62 | {
63 | uint nItems = br.ReadUInt32();
64 |
65 | List res = new();
66 |
67 | for (int i = 0; i < nItems; i++)
68 | {
69 | T s = ReadScalarValue(br)!;
70 |
71 | res.Add(s);
72 |
73 | // The padding in a vector can be per-item
74 | int itemSize = (int)(br.BaseStream.Position - currentPos);
75 |
76 | int pad = itemSize % 4;
77 | if (pad > 0 && NeedsPadding)
78 | br.ReadBytes(4 - pad); // padding
79 | }
80 |
81 | propertyValue = res;
82 | int size = (int)(br.BaseStream.Position - currentPos);
83 |
84 | int m = size % 4;
85 | if (m > 0 && NeedsPadding)
86 | br.ReadBytes(4 - m); // padding
87 | }
88 |
89 | break;
90 | default:
91 | break;
92 | }
93 | }
94 |
95 | public abstract void WriteScalarValue(BinaryWriter bw, T pValue);
96 |
97 | public void Write(BinaryWriter bw)
98 | {
99 | long currentPos = bw.BaseStream.Position;
100 | int size;
101 | int m;
102 | switch (PropertyDimensions)
103 | {
104 | case PropertyDimensions.IsScalar:
105 |
106 | bw.Write((ushort)_VTType);
107 | bw.Write((ushort)0);
108 |
109 | WriteScalarValue(bw, (T)propertyValue!);
110 | size = (int)(bw.BaseStream.Position - currentPos);
111 | m = size % 4;
112 |
113 | if (m > 0 && NeedsPadding)
114 | {
115 | for (int i = 0; i < 4 - m; i++) // padding
116 | bw.Write((byte)0);
117 | }
118 |
119 | break;
120 |
121 | case PropertyDimensions.IsVector:
122 |
123 | bw.Write((ushort)_VTType);
124 | bw.Write((ushort)0);
125 | bw.Write((uint)((List)propertyValue!).Count);
126 |
127 | for (int i = 0; i < ((List)propertyValue).Count; i++)
128 | {
129 | WriteScalarValue(bw, ((List)propertyValue)[i]);
130 |
131 | size = (int)(bw.BaseStream.Position - currentPos);
132 | m = size % 4;
133 |
134 | if (m > 0 && NeedsPadding)
135 | {
136 | for (int q = 0; q < 4 - m; q++) // padding
137 | bw.Write((byte)0);
138 | }
139 | }
140 |
141 | size = (int)(bw.BaseStream.Position - currentPos);
142 | m = size % 4;
143 |
144 | if (m > 0 && NeedsPadding)
145 | {
146 | for (int i = 0; i < 4 - m; i++) // padding
147 | bw.Write((byte)0);
148 | }
149 |
150 | break;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/OpenMcdf.Ole/VTPropertyType.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Ole;
2 |
3 | #pragma warning disable CA1707 // Remove the underscores from member name
4 |
5 | ///
6 | /// VARENUM
7 | ///
8 | public enum VTPropertyType : ushort
9 | {
10 | VT_EMPTY = 0x0000,
11 | VT_NULL = 0x0001,
12 | VT_I2 = 0x0002,
13 | VT_I4 = 0x0003,
14 | VT_R4 = 0x0004,
15 | VT_R8 = 0x0005,
16 | VT_CY = 0x0006,
17 | VT_DATE = 0x0007,
18 | VT_BSTR = 0x0008,
19 | VT_ERROR = 0x000A,
20 | VT_BOOL = 0x000B,
21 | VT_DECIMAL = 0x000E,
22 | VT_I1 = 0x0010,
23 | VT_UI1 = 0x0011,
24 | VT_UI2 = 0x0012,
25 | VT_UI4 = 0x0013,
26 | VT_I8 = 0x0014, // MUST be an 8-byte signed integer.
27 | VT_UI8 = 0x0015, // MUST be an 8-byte unsigned integer.
28 | VT_INT = 0x0016, // MUST be a 4-byte signed integer.
29 | VT_UINT = 0x0017, // MUST be a 4-byte unsigned integer.
30 | VT_LPSTR = 0x001E, // MUST be a CodePageString.
31 | VT_LPWSTR = 0x001F, // MUST be a UnicodeString.
32 | VT_FILETIME = 0x0040, // MUST be a FILETIME (Packet Version).
33 | VT_BLOB = 0x0041, // MUST be a BLOB.
34 | VT_STREAM = 0x0042, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a stream element with this name.
35 | VT_STORAGE = 0x0043, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a storage element with this name.
36 | VT_STREAMED_OBJECT = 0x0044, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a stream element with this name.
37 | VT_STORED_OBJECT = 0x0045, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a storage element with this name.
38 | VT_BLOB_OBJECT = 0x0046, // MUST be a BLOB.
39 | VT_CF = 0x0047, // MUST be a ClipboardData.
40 | VT_CLSID = 0x0048, // MUST be a GUID (Packet Version)
41 | VT_VERSIONED_STREAM = 0x0049, // MUST be a versioned Stream, NOT allowed in simple property
42 | VT_VECTOR_HEADER = 0x1000, //--- NOT NORMATIVE
43 | VT_ARRAY_HEADER = 0x2000, //--- NOT NORMATIVE
44 | VT_VARIANT_VECTOR = 0x000C, //--- NOT NORMATIVE
45 | VT_VARIANT_ARRAY = 0x200C, //--- NOT NORMATIVE
46 | }
47 |
--------------------------------------------------------------------------------
/OpenMcdf.Perf/OpenMcdf.Perf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0-windows
5 | Exe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/OpenMcdf.Perf/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace OpenMcdf.Perf;
4 |
5 | internal sealed class Program
6 | {
7 | static void Main(string[] args)
8 | {
9 | var stopwatch = Stopwatch.StartNew();
10 | int bufferLength = 1024 * 1024;
11 | int streamLength = 8 * 1024 * 1024;
12 | Write(Version.V3, StorageModeFlags.None, bufferLength, streamLength, 100);
13 | Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
14 | }
15 |
16 | public static void Write(Version version, StorageModeFlags storageModeFlags, int bufferLength, int streamLength, int iterations)
17 | {
18 | byte[] buffer = new byte[bufferLength];
19 |
20 | //using FileStream baseStream = File.Create(Path.GetTempFileName());
21 | using MemoryStream baseStream = new(streamLength * iterations * 2);
22 | for (int i = 0; i < iterations; i++)
23 | {
24 | using var rootStorage = RootStorage.Create(baseStream, version, storageModeFlags | StorageModeFlags.LeaveOpen);
25 | using Stream stream = rootStorage.CreateStream("TestStream");
26 |
27 | for (int j = 0; j < streamLength / bufferLength; j++)
28 | stream.Write(buffer, 0, buffer.Length);
29 |
30 | if (storageModeFlags.HasFlag(StorageModeFlags.Transacted))
31 | rootStorage.Commit();
32 | }
33 | }
34 |
35 | public static void MultiStorageAndStreamWrite()
36 | {
37 | int storageCount = 8;
38 | int streamCount = 8;
39 | int writeCount = 1024;
40 | byte[] buffer = new byte[32 * 512];
41 |
42 | Microsoft.IO.RecyclableMemoryStreamManager manager = new();
43 | Microsoft.IO.RecyclableMemoryStream baseStream = new(manager);
44 | baseStream.Capacity = 2 * (storageCount * buffer.Length * writeCount + storageCount * (streamCount - 1) * buffer.Length);
45 |
46 | using var rootStorage = RootStorage.Create(baseStream, Version.V4);
47 | for (int k = 0; k < storageCount; k++)
48 | {
49 | Console.WriteLine($"Creating Storage {k}");
50 | Storage storage = rootStorage.CreateStorage($"TestStorage{k}");
51 | for (int i = 0; i < streamCount; i++)
52 | {
53 | using CfbStream stream = storage.CreateStream($"TestStream{i}");
54 |
55 | int to = i == 0 ? writeCount : 1;
56 | for (int j = 0; j < to; j++)
57 | stream.Write(buffer, 0, buffer.Length);
58 | }
59 | }
60 | }
61 |
62 | public static void MultiStorageAndStreamWriteBaseline()
63 | {
64 | int storageCount = 8;
65 | int streamCount = 8;
66 | int writeCount = 1024;
67 | byte[] buffer = new byte[32 * 512];
68 | int capacity = 2 * (storageCount * buffer.Length * writeCount + storageCount * (streamCount - 1) * buffer.Length);
69 |
70 | using var rootStorage = StructuredStorage.Storage.CreateInMemory(capacity);
71 | for (int k = 0; k < storageCount; k++)
72 | {
73 | Console.WriteLine($"Creating Storage {k}");
74 | var storage = rootStorage.CreateStorage($"TestStorage{k}");
75 | for (int i = 0; i < streamCount; i++)
76 | {
77 | using var stream = storage.CreateStream($"TestStream{i}");
78 |
79 | int to = i == 0 ? writeCount : 1;
80 | for (int j = 0; j < to; j++)
81 | stream.Write(buffer, 0, buffer.Length);
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | // In SDK-style projects such as this one, several assembly attributes that were historically
4 | // defined in this file are now automatically added during build and populated with
5 | // values defined in project properties. For details of which attributes are included
6 | // and how to customize this process see: https://aka.ms/assembly-info-properties
7 |
8 |
9 | // Setting ComVisible to false makes the types in this assembly not visible to COM
10 | // components. If you need to access a type in this assembly from COM, set the ComVisible
11 | // attribute to true on that type.
12 |
13 | [assembly: ComVisible(false)]
14 |
15 | // The following GUID is for the ID of the typelib if this project is exposed to COM.
16 |
17 | [assembly: Guid("38e3c8a7-44c7-4d13-9c30-7aa75b038c83")]
18 |
19 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
20 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/BinaryReaderTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Tests;
2 |
3 | [TestClass]
4 | public sealed class BinaryReaderTests
5 | {
6 | [TestMethod]
7 | public void ReadGuid()
8 | {
9 | byte[] bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10];
10 | using MemoryStream stream = new(bytes);
11 | using CfbBinaryReader reader = new(stream);
12 | Guid guid = reader.ReadGuid();
13 | Assert.AreEqual(new Guid(bytes), guid);
14 |
15 | Assert.ThrowsException(() => reader.ReadGuid());
16 | }
17 |
18 | [TestMethod]
19 | public void ReadFileTime()
20 | {
21 | byte[] bytes = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
22 | using MemoryStream stream = new(bytes);
23 | using CfbBinaryReader reader = new(stream);
24 | DateTime actual = reader.ReadFileTime();
25 | Assert.AreEqual(DirectoryEntry.ZeroFileTime, actual);
26 | }
27 |
28 | [TestMethod]
29 | [DataRow("TestStream_v3_0.cfs")]
30 | [DataRow("TestStream_v4_0.cfs")]
31 | public void ReadHeader(string fileName)
32 | {
33 | using FileStream stream = File.OpenRead(fileName);
34 | using MemoryStream memoryStream = new();
35 | stream.CopyAllTo(memoryStream);
36 |
37 | using CfbBinaryReader reader = new(memoryStream);
38 | Header header = reader.ReadHeader();
39 |
40 | stream.CopyAllTo(memoryStream);
41 | memoryStream.WriteByte(1); // Corrupt signature
42 | Assert.ThrowsException(() => reader.ReadHeader());
43 |
44 | stream.CopyAllTo(memoryStream);
45 | memoryStream.Position = 24;
46 | memoryStream.WriteByte(1); // Corrupt CLSID
47 | Assert.ThrowsException(() => reader.ReadHeader());
48 |
49 | stream.CopyAllTo(memoryStream);
50 | memoryStream.Position = 26;
51 | memoryStream.WriteByte(1); // Corrupt Major version
52 | Assert.ThrowsException(() => reader.ReadHeader());
53 |
54 | stream.CopyAllTo(memoryStream);
55 | memoryStream.Position = 28;
56 | memoryStream.WriteByte(1); // Corrupt byte order
57 | Assert.ThrowsException(() => reader.ReadHeader());
58 |
59 | stream.CopyAllTo(memoryStream);
60 | memoryStream.Position = 32;
61 | memoryStream.WriteByte(1); // Corrupt mini sector shift
62 | Assert.ThrowsException(() => reader.ReadHeader());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/BinaryWriterTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace OpenMcdf.Tests;
4 |
5 | [TestClass]
6 | public sealed class BinaryWriterTests
7 | {
8 | [TestMethod]
9 | public void WriteGuid()
10 | {
11 | byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 };
12 | Guid expectedGuid = new(bytes);
13 | using MemoryStream stream = new(bytes);
14 | using (CfbBinaryWriter writer = new(stream))
15 | writer.Write(expectedGuid);
16 |
17 | stream.Position = 0;
18 | using CfbBinaryReader reader = new(stream);
19 | Guid actualGuid = reader.ReadGuid();
20 |
21 | Assert.AreEqual(expectedGuid, actualGuid);
22 | }
23 |
24 | [TestMethod]
25 | [DataRow("TestStream_v3_0.cfs")]
26 | [DataRow("TestStream_v4_0.cfs")]
27 | public void WriteHeader(string fileName)
28 | {
29 | using FileStream stream = File.OpenRead(fileName);
30 | using CfbBinaryReader reader = new(stream);
31 | Header header = reader.ReadHeader();
32 |
33 | using MemoryStream memoryStream = new();
34 | using CfbBinaryWriter writer = new(memoryStream);
35 | writer.Write(header);
36 |
37 | memoryStream.Position = 0;
38 | using CfbBinaryReader reader2 = new(memoryStream);
39 | Header actualHeader = reader2.ReadHeader();
40 |
41 | Assert.AreEqual(header, actualHeader);
42 | }
43 |
44 | [TestMethod]
45 | public void WriteDirectoryEntry()
46 | {
47 | DirectoryEntry expected = new()
48 | {
49 | Type = StorageType.Storage,
50 | Color = NodeColor.Red,
51 | LeftSiblingId = 2,
52 | RightSiblingId = 3,
53 | ChildId = 4,
54 | CLSID = Guid.NewGuid(),
55 | StateBits = 5,
56 | CreationTime = DateTime.UtcNow,
57 | ModifiedTime = DateTime.UtcNow,
58 | StartSectorId = 6,
59 | StreamLength = 7
60 | };
61 |
62 | string name = "Root Entry";
63 | expected.NameLength = (ushort)Encoding.Unicode.GetBytes(name, 0, name.Length, expected.Name, 0);
64 |
65 | using MemoryStream stream = new();
66 | using CfbBinaryWriter writer = new(stream);
67 | writer.Write(expected);
68 |
69 | stream.Position = 0;
70 | using CfbBinaryReader reader = new(stream);
71 | DirectoryEntry actual = reader.ReadDirectoryEntry(Version.V4, 0);
72 |
73 | Assert.AreEqual(expected, actual);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/DebugWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text;
3 |
4 | namespace OpenMcdf.Tests;
5 |
6 | internal sealed class DebugWriter : TextWriter
7 | {
8 | public static DebugWriter Default { get; } = new();
9 |
10 | public override Encoding Encoding => Encoding.Unicode;
11 |
12 | public override void Write(char value) => Debug.Write(value);
13 |
14 | public override void Write(string? value) => Debug.Write(value);
15 |
16 | public override void WriteLine(string? value) => Debug.WriteLine(value);
17 | }
18 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/DirectoryEntryTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Tests;
2 |
3 | [TestClass]
4 | public sealed class DirectoryEntryTests
5 | {
6 | [TestMethod]
7 | [DataRow("", "", 0)]
8 | [DataRow("", "longer", -1)]
9 | [DataRow("longer", "", 1)]
10 | [DataRow("a", "a", 0)]
11 | [DataRow("a", "b", -1)]
12 | [DataRow("b", "a", 1)]
13 | public void EnumerateEntryInfos(string nameX, string nameY, int expectedCompare)
14 | {
15 | DirectoryEntry entryX = new()
16 | {
17 | NameString = nameX,
18 | };
19 |
20 | DirectoryEntry entryY = new()
21 | {
22 | NameString = nameY,
23 | };
24 |
25 | int actualCompare = DirectoryEntryComparer.Default.Compare(entryX, entryY);
26 | Assert.AreEqual(expectedCompare, actualCompare);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/EntryInfoTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Tests;
2 |
3 | [TestClass]
4 | public sealed class EntryInfoTests
5 | {
6 | [TestMethod]
7 | [DataRow("MultipleStorage.cfs", EntryType.Storage, "MyStorage", "/Root Entry")]
8 | [DataRow("TestStream_v3_0.cfs", EntryType.Stream, "TestStream", "/Root Entry")]
9 | [DataRow("TestStream_v4_0.cfs", EntryType.Stream, "TestStream", "/Root Entry")]
10 | public void EnumerateEntryInfos(string fileName, EntryType type, string name, string path)
11 | {
12 | using var rootStorage = RootStorage.OpenRead(fileName);
13 | IEnumerable entries = rootStorage.EnumerateEntries();
14 | Assert.AreEqual(1, entries.Count());
15 |
16 | EntryInfo entry = entries.First();
17 | Assert.AreEqual(type, entry.Type);
18 | Assert.AreEqual(name, entry.Name);
19 | Assert.AreEqual(path, entry.Path);
20 | Assert.AreEqual(Guid.Empty, entry.CLSID);
21 | Assert.AreEqual(0, entry.Length);
22 | if (type is EntryType.Storage)
23 | {
24 | Assert.AreNotEqual(DirectoryEntry.ZeroFileTime, entry.CreationTime);
25 | Assert.AreNotEqual(DirectoryEntry.ZeroFileTime, entry.ModifiedTime);
26 | }
27 | else
28 | {
29 | Assert.AreEqual(DirectoryEntry.ZeroFileTime, entry.CreationTime);
30 | Assert.AreEqual(DirectoryEntry.ZeroFileTime, entry.ModifiedTime);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/FatChainLoop_v3.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/FatChainLoop_v3.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Tests/MultipleStorage.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/MultipleStorage.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Tests/MultipleStorage2.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/MultipleStorage2.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Tests/MultipleStorage3.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/MultipleStorage3.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Tests/MultipleStorage4.cfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/MultipleStorage4.cfs
--------------------------------------------------------------------------------
/OpenMcdf.Tests/OpenMcdf.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | net48;net8.0-windows
6 | Exe
7 |
8 | false
9 | true
10 | true
11 |
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 | all
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | PreserveNewest
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/StreamAssert.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Tests;
2 |
3 | internal static class StreamAssert
4 | {
5 | public static void AreEqual(Stream expected, Stream actual, int bufferLength = 4096)
6 | {
7 | Assert.AreEqual(expected.Length, actual.Length);
8 |
9 | expected.Position = 0;
10 | actual.Position = 0;
11 |
12 | byte[] expectedBuffer = new byte[bufferLength];
13 | byte[] actualBuffer = new byte[bufferLength];
14 | while (expected.Position < expected.Length)
15 | {
16 | int expectedRead = expected.Read(expectedBuffer, 0, expectedBuffer.Length);
17 | int actualRead = actual.Read(actualBuffer, 0, actualBuffer.Length);
18 |
19 | if (expectedRead == bufferLength && actualRead == bufferLength)
20 | CollectionAssert.AreEqual(expectedBuffer, actualBuffer);
21 | else
22 | CollectionAssert.AreEqual(expectedBuffer.Take(expectedRead).ToList(), actualBuffer.Take(actualRead).ToList());
23 | }
24 |
25 | Assert.AreEqual(expected.Position, actual.Position);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | #if !NET7_0_OR_GREATER
2 | using System.Buffers;
3 | #endif
4 |
5 | namespace OpenMcdf.Tests;
6 |
7 | internal static class StreamExtensions
8 | {
9 | #if !NET7_0_OR_GREATER
10 | public static void ReadExactly(this Stream stream, Span buffer)
11 | {
12 | byte[] array = ArrayPool.Shared.Rent(buffer.Length);
13 | try
14 | {
15 | stream.ReadExactly(array, 0, buffer.Length);
16 | array.AsSpan(0, buffer.Length).CopyTo(buffer);
17 | }
18 | finally
19 | {
20 | ArrayPool.Shared.Return(array);
21 | }
22 | }
23 | #endif
24 |
25 | public static void Write(this Stream stream, byte[] buffer, WriteMode mode)
26 | {
27 | #if (!NETSTANDARD2_0 && !NETFRAMEWORK)
28 | switch (mode)
29 | {
30 | case WriteMode.Array:
31 | stream.Write(buffer, 0, buffer.Length);
32 | break;
33 | case WriteMode.Span:
34 | stream.Write(buffer);
35 | break;
36 | case WriteMode.SingleByte:
37 | {
38 | for (int i = 0; i < buffer.Length; i++)
39 | stream.WriteByte(buffer[i]);
40 | break;
41 | }
42 | }
43 | #else
44 | stream.Write(buffer, 0, buffer.Length);
45 | #endif
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/OpenMcdf.Tests/TestData.cs:
--------------------------------------------------------------------------------
1 | namespace OpenMcdf.Tests;
2 |
3 | internal static class TestData
4 | {
5 | ///
6 | /// Fill with bytes equal to their position modulo 256
7 | ///
8 | public static byte[] CreateByteArray(int length)
9 | {
10 | byte[] expectedBuffer = new byte[length];
11 | for (int i = 0; i < length; i++)
12 | expectedBuffer[i] = (byte)i;
13 | return expectedBuffer;
14 | }
15 |
16 | public static IEnumerable