├── .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 ShortVersionsAndSizes { get; } = new[] 17 | { 18 | new object[] { Version.V3, 0 }, 19 | [Version.V3, 63], 20 | [Version.V3, 64], // Mini-stream sector size 21 | [Version.V3, 65], 22 | [Version.V3, 511], 23 | [Version.V3, 512], // Multiple stream sectors 24 | [Version.V3, 513], 25 | [Version.V3, 4095], 26 | [Version.V3, 4096], 27 | [Version.V3, 4097], 28 | [Version.V4, 0], 29 | [Version.V4, 63], 30 | [Version.V4, 64], // Mini-stream sector size 31 | [Version.V4, 65], 32 | [Version.V4, 511], 33 | [Version.V4, 512], 34 | [Version.V4, 513], 35 | [Version.V4, 4095], 36 | [Version.V4, 4096], // Multiple stream sectors 37 | [Version.V4, 4097], 38 | }; 39 | 40 | public static IEnumerable VersionsAndSizes { get; } = new[] 41 | { 42 | new object[] { Version.V3, 0 }, 43 | [Version.V3, 63], 44 | [Version.V3, 64], // Mini-stream sector size 45 | [Version.V3, 65], 46 | [Version.V3, 511], 47 | [Version.V3, 512], // Multiple stream sectors 48 | [Version.V3, 513], 49 | [Version.V3, 4095], 50 | [Version.V3, 4096], 51 | [Version.V3, 4097], 52 | [Version.V3, 128 * 512], // Multiple FAT sectors 53 | [Version.V3, 1024 * 4096], // Multiple FAT sectors 54 | [Version.V3, 7087616], // First DIFAT chain 55 | [Version.V3, 2 * 7087616], // Long DIFAT chain 56 | [Version.V4, 0], 57 | [Version.V4, 63], 58 | [Version.V4, 64], // Mini-stream sector size 59 | [Version.V4, 65], 60 | [Version.V4, 511], 61 | [Version.V4, 512], 62 | [Version.V4, 513], 63 | [Version.V4, 4095], 64 | [Version.V4, 4096], // Multiple stream sectors 65 | [Version.V4, 4097], 66 | [Version.V4, 1024 * 4096], // Multiple FAT sectors (1024 * 4096 67 | [Version.V4, 7087616 * 4], // First DIFAT chain 68 | [Version.V4, 2 * 7087616 * 4], // Long DIFAT chain 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_0.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_0.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_4095.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_4095.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_4096.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_4096.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_4097.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_4097.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_511.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_511.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_512.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_512.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_513.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_513.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_63.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_63.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_64.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_64.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_65.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_65.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v3_65536.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v3_65536.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_0.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_0.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_4095.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_4095.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_4096.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_4096.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_4097.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_4097.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_511.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_511.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_512.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_512.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_513.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_513.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_63.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_63.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_64.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_64.cfs -------------------------------------------------------------------------------- /OpenMcdf.Tests/TestStream_v4_65.cfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/OpenMcdf.Tests/TestStream_v4_65.cfs -------------------------------------------------------------------------------- /OpenMcdf.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35327.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf", "OpenMcdf\OpenMcdf.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Tests", "OpenMcdf.Tests\OpenMcdf.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34030FA7-0A06-43D1-85DD-ADD39D502C3C}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | Directory.Build.props = Directory.Build.props 14 | Directory.Build.targets = Directory.Build.targets 15 | Directory.csproj.props = Directory.csproj.props 16 | Directory.csproj.targets = Directory.csproj.targets 17 | Directory.Packages.props = Directory.Packages.props 18 | exclusion.dic = exclusion.dic 19 | License.txt = License.txt 20 | README.md = README.md 21 | ReleaseNotes.md = ReleaseNotes.md 22 | EndProjectSection 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Benchmarks", "OpenMcdf.Benchmarks\OpenMcdf.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Perf", "OpenMcdf.Perf\OpenMcdf.Perf.csproj", "{8167F453-A244-4FE2-9B33-A7B80B1B7AB1}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorage", "StructuredStorage\StructuredStorage.csproj", "{D7861D73-B42C-403E-9B9E-F921BC70F0D3}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Ole", "OpenMcdf.Ole\OpenMcdf.Ole.csproj", "{06FFA945-128E-43FA-B541-38987BC1E0D5}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorageExplorer", "StructuredStorageExplorer\StructuredStorageExplorer.csproj", "{D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}" 33 | EndProject 34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf.Ole.Tests", "OpenMcdf.Ole.Tests\OpenMcdf.Ole.Tests.csproj", "{34F153C4-3EFA-4D6E-B860-AEE300CCCF98}" 35 | EndProject 36 | Global 37 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 38 | Debug|Any CPU = Debug|Any CPU 39 | Release|Any CPU = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 42 | {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {06FFA945-128E-43FA-B541-38987BC1E0D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {06FFA945-128E-43FA-B541-38987BC1E0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Release|Any CPU.Build.0 = Release|Any CPU 74 | EndGlobalSection 75 | GlobalSection(SolutionProperties) = preSolution 76 | HideSolutionNode = FALSE 77 | EndGlobalSection 78 | GlobalSection(ExtensibilityGlobals) = postSolution 79 | SolutionGuid = {927A4DA2-8926-4AC2-A48C-976978B5F2AA} 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /OpenMcdf/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | // In SDK-style projects such as this one, several assembly attributes that were historically 5 | // defined in this file are now automatically added during build and populated with 6 | // values defined in project properties. For details of which attributes are included 7 | // and how to customize this process see: https://aka.ms/assembly-info-properties 8 | 9 | 10 | // Setting ComVisible to false makes the types in this assembly not visible to COM 11 | // components. If you need to access a type in this assembly from COM, set the ComVisible 12 | // attribute to true on that type. 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | // The following GUID is for the ID of the typelib if this project is exposed to COM. 17 | 18 | [assembly: Guid("a96ebb34-8c16-4c7e-b9f7-651ba754b722")] 19 | [assembly: InternalsVisibleTo("OpenMcdf.Tests")] 20 | -------------------------------------------------------------------------------- /OpenMcdf/CfbBinaryReader.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text; 3 | 4 | namespace OpenMcdf; 5 | 6 | /// 7 | /// Reads CFB data types from a stream. 8 | /// 9 | internal sealed class CfbBinaryReader : BinaryReader 10 | { 11 | readonly byte[] guidBuffer = new byte[16]; 12 | readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; 13 | 14 | public CfbBinaryReader(Stream input) 15 | : base(input, Encoding.Unicode, true) 16 | { 17 | } 18 | 19 | public long Position 20 | { 21 | get => BaseStream.Position; 22 | set => BaseStream.Position = value; 23 | } 24 | 25 | void ReadExactly(byte[] buffer, int offset, int count) => BaseStream.ReadExactly(buffer, offset, count); 26 | 27 | public Guid ReadGuid() 28 | { 29 | BaseStream.ReadExactly(guidBuffer, 0, guidBuffer.Length); 30 | 31 | return new Guid(guidBuffer); 32 | } 33 | 34 | public DateTime ReadFileTime() 35 | { 36 | long fileTime = ReadInt64(); 37 | return DateTime.FromFileTimeUtc(fileTime); 38 | } 39 | 40 | public Header ReadHeader() 41 | { 42 | Header header = new(); 43 | ReadExactly(buffer, 0, Header.Signature.Length); 44 | Span signature = buffer.AsSpan(0, Header.Signature.Length); 45 | if (!signature.SequenceEqual(Header.Signature)) 46 | throw new FileFormatException("Invalid header signature."); 47 | header.CLSID = ReadGuid(); 48 | if (header.CLSID != Guid.Empty) 49 | throw new FileFormatException($"Invalid header CLSID: {header.CLSID}."); 50 | header.MinorVersion = ReadUInt16(); 51 | header.MajorVersion = ReadUInt16(); 52 | if (header.MajorVersion is not (ushort)Version.V3 and not (ushort)Version.V4) 53 | throw new FileFormatException($"Unsupported major version: {header.MajorVersion}."); 54 | else if (header.MinorVersion is not Header.ExpectedMinorVersion) 55 | Trace.WriteLine($"Unexpected minor version: {header.MinorVersion}."); 56 | ushort byteOrder = ReadUInt16(); 57 | if (byteOrder != Header.LittleEndian) 58 | throw new FileFormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})."); 59 | header.SectorShift = ReadUInt16(); 60 | header.MiniSectorShift = ReadUInt16(); 61 | FillBuffer(6); 62 | header.DirectorySectorCount = ReadUInt32(); 63 | header.FatSectorCount = ReadUInt32(); 64 | header.FirstDirectorySectorId = ReadUInt32(); 65 | FillBuffer(4); 66 | uint miniStreamCutoffSize = ReadUInt32(); 67 | if (miniStreamCutoffSize != Header.MiniStreamCutoffSize) 68 | throw new FileFormatException($"Mini stream cutoff size must be {Header.MiniStreamCutoffSize} bytes."); 69 | header.FirstMiniFatSectorId = ReadUInt32(); 70 | header.MiniFatSectorCount = ReadUInt32(); 71 | header.FirstDifatSectorId = ReadUInt32(); 72 | header.DifatSectorCount = ReadUInt32(); 73 | 74 | for (int i = 0; i < Header.DifatArrayLength; i++) 75 | { 76 | header.Difat[i] = ReadUInt32(); 77 | } 78 | 79 | return header; 80 | } 81 | 82 | public StorageType ReadStorageType() 83 | { 84 | var type = (StorageType)ReadByte(); 85 | if (type is not StorageType.Storage and not StorageType.Stream and not StorageType.Root and not StorageType.Unallocated) 86 | throw new FileFormatException($"Invalid storage type: {type}."); 87 | return type; 88 | } 89 | 90 | public NodeColor ReadColor() 91 | { 92 | var color = (NodeColor)ReadByte(); 93 | if (color is not NodeColor.Black and not NodeColor.Red) 94 | throw new FileFormatException($"Invalid node color: {color}."); 95 | return color; 96 | } 97 | 98 | public DirectoryEntry ReadDirectoryEntry(Version version, uint sid) 99 | { 100 | if (version is not Version.V3 and not Version.V4) 101 | throw new ArgumentException($"Unsupported version: {version}.", nameof(version)); 102 | 103 | ReadExactly(buffer, 0, DirectoryEntry.NameFieldLength); 104 | 105 | DirectoryEntry entry = new() 106 | { 107 | Id = sid, 108 | NameLength = ReadUInt16(), 109 | Type = ReadStorageType(), 110 | Color = ReadColor(), 111 | LeftSiblingId = ReadUInt32(), 112 | RightSiblingId = ReadUInt32(), 113 | ChildId = ReadUInt32(), 114 | CLSID = ReadGuid(), 115 | StateBits = ReadUInt32(), 116 | CreationTime = ReadFileTime(), 117 | ModifiedTime = ReadFileTime(), 118 | StartSectorId = ReadUInt32() 119 | }; 120 | 121 | Buffer.BlockCopy(buffer, 0, entry.Name, 0, DirectoryEntry.NameFieldLength); 122 | 123 | if (version == Version.V3) 124 | { 125 | entry.StreamLength = ReadUInt32(); 126 | if (entry.StreamLength > DirectoryEntry.MaxV3StreamLength) 127 | throw new FileFormatException($"Stream length {entry.StreamLength} exceeds maximum value {DirectoryEntry.MaxV3StreamLength}."); 128 | ReadUInt32(); // Skip unused 4 bytes 129 | } 130 | else if (version == Version.V4) 131 | { 132 | entry.StreamLength = ReadInt64(); 133 | } 134 | 135 | return entry; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /OpenMcdf/CfbBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Writes CFB data types to a stream. 7 | /// 8 | internal sealed class CfbBinaryWriter : BinaryWriter 9 | { 10 | public CfbBinaryWriter(Stream input) 11 | : base(input, Encoding.Unicode, true) 12 | { 13 | } 14 | 15 | public long Position 16 | { 17 | get => BaseStream.Position; 18 | set => BaseStream.Position = value; 19 | } 20 | 21 | #if (!NETSTANDARD2_0 && !NETFRAMEWORK) 22 | 23 | public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); 24 | 25 | #endif 26 | 27 | public void Write(in Guid value) 28 | { 29 | #if NETSTANDARD2_0 || NETFRAMEWORK 30 | byte[] bytes = value.ToByteArray(); 31 | Write(bytes); 32 | #else 33 | Span localBuffer = stackalloc byte[16]; 34 | value.TryWriteBytes(localBuffer); 35 | Write(localBuffer); 36 | #endif 37 | } 38 | 39 | public void Write(DateTime value) 40 | { 41 | long fileTime = value.ToFileTimeUtc(); 42 | Write(fileTime); 43 | } 44 | 45 | public void Write(Header header) 46 | { 47 | Write(Header.Signature); 48 | Write(header.CLSID); 49 | Write(header.MinorVersion); 50 | Write(header.MajorVersion); 51 | Write(Header.LittleEndian); 52 | Write(header.SectorShift); 53 | Write(header.MiniSectorShift); 54 | Write(Header.Unused); 55 | Write(header.DirectorySectorCount); 56 | Write(header.FatSectorCount); 57 | Write(header.FirstDirectorySectorId); 58 | Write((uint)0); 59 | Write(Header.MiniStreamCutoffSize); 60 | Write(header.FirstMiniFatSectorId); 61 | Write(header.MiniFatSectorCount); 62 | Write(header.FirstDifatSectorId); 63 | Write(header.DifatSectorCount); 64 | for (int i = 0; i < Header.DifatArrayLength; i++) 65 | Write(header.Difat[i]); 66 | } 67 | 68 | public void Write(DirectoryEntry entry) 69 | { 70 | Write(entry.Name, 0, DirectoryEntry.NameFieldLength); 71 | Write(entry.NameLength); 72 | Write((byte)entry.Type); 73 | Write((byte)entry.Color); 74 | Write(entry.LeftSiblingId); 75 | Write(entry.RightSiblingId); 76 | Write(entry.ChildId); 77 | Write(entry.CLSID); 78 | Write(entry.StateBits); 79 | Write(entry.CreationTime); 80 | Write(entry.ModifiedTime); 81 | Write(entry.StartSectorId); 82 | Write(entry.StreamLength); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /OpenMcdf/CfbStream.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// Represents a stream in a compound file. 5 | /// 6 | public sealed class CfbStream : Stream 7 | { 8 | private readonly RootContextSite rootContextSite; 9 | private readonly DirectoryEntry directoryEntry; 10 | private Stream stream; 11 | private bool isDisposed; 12 | 13 | internal CfbStream(RootContextSite rootContextSite, DirectoryEntry directoryEntry, Storage parent) 14 | { 15 | this.rootContextSite = rootContextSite; 16 | this.directoryEntry = directoryEntry; 17 | Parent = parent; 18 | stream = directoryEntry.StreamLength < Header.MiniStreamCutoffSize 19 | ? new MiniFatStream(rootContextSite, directoryEntry) 20 | : new FatStream(rootContextSite, directoryEntry); 21 | } 22 | 23 | protected override void Dispose(bool disposing) 24 | { 25 | if (!isDisposed) 26 | { 27 | stream.Dispose(); 28 | isDisposed = true; 29 | } 30 | 31 | base.Dispose(disposing); 32 | } 33 | 34 | public Storage Parent { get; } 35 | 36 | public EntryInfo EntryInfo 37 | { 38 | get 39 | { 40 | EntryInfo parentEntryInfo = Parent.EntryInfo; 41 | string path = $"{parentEntryInfo.Path}{parentEntryInfo.Name}"; 42 | return directoryEntry.ToEntryInfo(path); 43 | } 44 | } 45 | 46 | public override bool CanRead => stream.CanRead; 47 | 48 | public override bool CanSeek => stream.CanSeek; 49 | 50 | public override bool CanWrite => stream.CanWrite; 51 | 52 | public override long Length => stream.Length; 53 | 54 | public override long Position { get => stream.Position; set => stream.Position = value; } 55 | 56 | public override void Flush() 57 | { 58 | this.ThrowIfDisposed(isDisposed); 59 | 60 | stream.Flush(); 61 | } 62 | 63 | public override int Read(byte[] buffer, int offset, int count) 64 | { 65 | ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); 66 | 67 | this.ThrowIfDisposed(isDisposed); 68 | 69 | return stream.Read(buffer, offset, count); 70 | } 71 | 72 | public override long Seek(long offset, SeekOrigin origin) 73 | { 74 | this.ThrowIfDisposed(isDisposed); 75 | 76 | return stream.Seek(offset, origin); 77 | } 78 | 79 | private void EnsureLengthToWrite(int count) 80 | { 81 | long newPosition = Position + count; 82 | if (newPosition > stream.Length) 83 | SetLength(newPosition); 84 | } 85 | 86 | public override void SetLength(long value) 87 | { 88 | if (value < 0) 89 | throw new ArgumentOutOfRangeException(nameof(value)); 90 | 91 | this.ThrowIfDisposed(isDisposed); 92 | this.ThrowIfNotWritable(); 93 | 94 | if (value >= Header.MiniStreamCutoffSize && stream is MiniFatStream miniStream) 95 | { 96 | long position = miniStream.Position; 97 | miniStream.Position = 0; 98 | 99 | DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); 100 | FatStream fatStream = new(rootContextSite, newDirectoryEntry); 101 | fatStream.SetLength(value); // Reserve enough space up front 102 | miniStream.CopyTo(fatStream); 103 | fatStream.Position = position; 104 | stream = fatStream; 105 | 106 | miniStream.SetLength(0); 107 | miniStream.Dispose(); 108 | } 109 | else if (value < Header.MiniStreamCutoffSize && stream is FatStream fatStream) 110 | { 111 | long position = fatStream.Position; 112 | fatStream.Position = 0; 113 | 114 | DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); 115 | MiniFatStream miniFatStream = new(rootContextSite, newDirectoryEntry); 116 | fatStream.SetLength(value); // Truncate the stream 117 | fatStream.CopyTo(miniFatStream); 118 | miniFatStream.Position = position; 119 | stream = miniFatStream; 120 | 121 | fatStream.SetLength(0); 122 | fatStream.Dispose(); 123 | } 124 | else 125 | { 126 | stream.SetLength(value); 127 | } 128 | } 129 | 130 | public override void Write(byte[] buffer, int offset, int count) 131 | { 132 | ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); 133 | 134 | this.ThrowIfDisposed(isDisposed); 135 | this.ThrowIfNotWritable(); 136 | 137 | EnsureLengthToWrite(count); 138 | 139 | stream.Write(buffer, offset, count); 140 | } 141 | 142 | #if (!NETSTANDARD2_0 && !NETFRAMEWORK) 143 | 144 | public override int Read(Span buffer) 145 | { 146 | this.ThrowIfDisposed(isDisposed); 147 | 148 | return stream.Read(buffer); 149 | } 150 | 151 | public override int ReadByte() => this.ReadByteCore(); 152 | 153 | public override void WriteByte(byte value) => this.WriteByteCore(value); 154 | 155 | public override void Write(ReadOnlySpan buffer) 156 | { 157 | this.ThrowIfDisposed(isDisposed); 158 | this.ThrowIfNotWritable(); 159 | 160 | EnsureLengthToWrite(buffer.Length); 161 | 162 | stream.Write(buffer); 163 | } 164 | 165 | #endif 166 | } 167 | -------------------------------------------------------------------------------- /OpenMcdf/ContextBase.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// Supports switching the object. 5 | /// 6 | public abstract class ContextBase 7 | { 8 | internal RootContextSite ContextSite { get; } 9 | 10 | internal RootContext Context => ContextSite.Context; 11 | 12 | internal ContextBase(RootContextSite site) 13 | { 14 | ContextSite = site; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /OpenMcdf/DifatSectorEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Enumerates the s in a DIFAT chain. 7 | /// 8 | internal class DifatSectorEnumerator : ContextBase, IEnumerator 9 | { 10 | bool start = true; 11 | uint index = uint.MaxValue; 12 | Sector current = Sector.EndOfChain; 13 | private uint difatSectorId = SectorType.EndOfChain; 14 | 15 | public DifatSectorEnumerator(RootContextSite rootContextSite) 16 | : base(rootContextSite) 17 | { 18 | } 19 | 20 | /// 21 | public void Dispose() 22 | { 23 | } 24 | 25 | /// 26 | public Sector Current 27 | { 28 | get 29 | { 30 | if (current.Id == SectorType.EndOfChain) 31 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 32 | return current; 33 | } 34 | } 35 | 36 | /// 37 | object IEnumerator.Current => Current; 38 | 39 | /// 40 | public bool MoveNext() 41 | { 42 | if (start) 43 | { 44 | start = false; 45 | index = uint.MaxValue; 46 | difatSectorId = Context.Header.FirstDifatSectorId; 47 | } 48 | 49 | uint nextIndex = index + 1; 50 | if (difatSectorId == SectorType.EndOfChain) 51 | { 52 | index = uint.MaxValue; 53 | current = Sector.EndOfChain; 54 | difatSectorId = SectorType.EndOfChain; 55 | return false; 56 | } 57 | 58 | current = new(difatSectorId, Context.SectorSize); 59 | index = nextIndex; 60 | Context.Reader.Position = current.EndPosition - sizeof(uint); 61 | difatSectorId = Context.Reader.ReadUInt32(); 62 | return true; 63 | } 64 | 65 | public bool MoveTo(uint index) 66 | { 67 | if (index >= Context.Header.DifatSectorCount) 68 | return false; 69 | 70 | if (start && !MoveNext()) 71 | return false; 72 | 73 | if (index < this.index) 74 | Reset(); 75 | 76 | while (start || this.index < index) 77 | { 78 | if (!MoveNext()) 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | /// 86 | public void Reset() 87 | { 88 | start = true; 89 | index = uint.MaxValue; 90 | current = Sector.EndOfChain; 91 | difatSectorId = SectorType.EndOfChain; 92 | } 93 | 94 | public void Add() 95 | { 96 | Sector newDifatSector = new(Context.SectorCount, Context.SectorSize); 97 | 98 | Header header = Context.Header; 99 | CfbBinaryWriter writer = Context.Writer; 100 | if (header.FirstDifatSectorId == SectorType.EndOfChain) 101 | { 102 | header.FirstDifatSectorId = newDifatSector.Id; 103 | } 104 | else 105 | { 106 | bool ok = MoveTo(header.DifatSectorCount - 1); 107 | if (!ok) 108 | throw new FileFormatException("The DIFAT sector count is invalid."); 109 | 110 | writer.Position = current.EndPosition - sizeof(uint); 111 | writer.Write(newDifatSector.Id); 112 | } 113 | 114 | writer.Position = newDifatSector.Position; 115 | writer.Write(SectorDataCache.GetFatEntryData(newDifatSector.Length)); 116 | writer.Position = newDifatSector.EndPosition - sizeof(uint); 117 | writer.Write(SectorType.EndOfChain); 118 | 119 | Context.ExtendStreamLength(newDifatSector.EndPosition); 120 | header.DifatSectorCount++; 121 | 122 | Context.Fat[newDifatSector.Id] = SectorType.Difat; 123 | 124 | start = false; 125 | index = header.DifatSectorCount - 1; 126 | current = newDifatSector; 127 | difatSectorId = SectorType.EndOfChain; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /OpenMcdf/DirectoryEntries.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Encapsulates getting and adding objects." 7 | /// 8 | internal sealed class DirectoryEntries : ContextBase, IDisposable 9 | { 10 | private readonly FatChainEnumerator fatChainEnumerator; 11 | private readonly DirectoryEntryEnumerator directoryEntryEnumerator; 12 | 13 | public DirectoryEntries(RootContextSite rootContextSite) 14 | : base(rootContextSite) 15 | { 16 | fatChainEnumerator = new FatChainEnumerator(Context.Fat, Context.Header.FirstDirectorySectorId); 17 | directoryEntryEnumerator = new DirectoryEntryEnumerator(this); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | directoryEntryEnumerator.Dispose(); 23 | fatChainEnumerator.Dispose(); 24 | } 25 | 26 | /// 27 | /// Gets the for the specified stream ID. 28 | /// 29 | public DirectoryEntry GetDictionaryEntry(uint streamId) 30 | { 31 | if (!TryGetDictionaryEntry(streamId, out DirectoryEntry? entry)) 32 | throw new FileFormatException($"Directory entry {streamId} was not found."); 33 | return entry!; 34 | } 35 | 36 | public bool TryGetDictionaryEntry(uint streamId, [MaybeNullWhen(false)] out DirectoryEntry entry) 37 | { 38 | if (streamId == StreamId.NoStream) 39 | { 40 | entry = null; 41 | return false; 42 | } 43 | 44 | if (streamId > StreamId.Maximum) 45 | throw new FileFormatException($"Invalid directory entry stream ID: ${streamId:X8}."); 46 | 47 | uint chainIndex = GetChainIndexAndEntryIndex(streamId, out long entryIndex); 48 | if (!fatChainEnumerator.MoveTo(chainIndex)) 49 | { 50 | entry = null; 51 | return false; 52 | } 53 | 54 | Context.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); 55 | entry = Context.Reader.ReadDirectoryEntry(Context.Version, streamId); 56 | return true; 57 | } 58 | 59 | private uint GetChainIndexAndEntryIndex(uint streamId, out long entryIndex) => (uint)Math.DivRem(streamId, Context.DirectoryEntriesPerSector, out entryIndex); 60 | 61 | public DirectoryEntry CreateOrRecycleDirectoryEntry() 62 | { 63 | DirectoryEntry? entry = TryRecycleDirectoryEntry(); 64 | if (entry is not null) 65 | return entry; 66 | 67 | CfbBinaryWriter writer = Context.Writer; 68 | uint id = fatChainEnumerator.Extend(); 69 | Header header = Context.Header; 70 | if (header.FirstDirectorySectorId == SectorType.EndOfChain) 71 | header.FirstDirectorySectorId = id; 72 | if (Context.Version == Version.V4) 73 | header.DirectorySectorCount++; 74 | 75 | Sector sector = new(id, Context.SectorSize); 76 | writer.Position = sector.Position; 77 | for (int i = 0; i < Context.DirectoryEntriesPerSector; i++) 78 | writer.Write(DirectoryEntry.Unallocated); 79 | 80 | entry = TryRecycleDirectoryEntry() 81 | ?? throw new InvalidOperationException("Failed to add or recycle directory entry."); 82 | return entry; 83 | } 84 | 85 | private DirectoryEntry? TryRecycleDirectoryEntry() 86 | { 87 | directoryEntryEnumerator.Reset(); 88 | 89 | while (directoryEntryEnumerator.MoveNext()) 90 | { 91 | DirectoryEntry current = directoryEntryEnumerator.Current; 92 | if (directoryEntryEnumerator.Current.Type is StorageType.Unallocated) 93 | return current; 94 | } 95 | 96 | return null; 97 | } 98 | 99 | public void Write(DirectoryEntry entry) 100 | { 101 | uint chainIndex = GetChainIndexAndEntryIndex(entry.Id, out long entryIndex); 102 | if (!fatChainEnumerator.MoveTo(chainIndex)) 103 | throw new FileFormatException($"Directory entry {entry.Id} was not found."); 104 | 105 | CfbBinaryWriter writer = Context.Writer; 106 | writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); 107 | writer.Write(entry); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /OpenMcdf/DirectoryEntryComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Provides a for objects. 7 | /// 8 | internal class DirectoryEntryComparer : IComparer 9 | { 10 | public static DirectoryEntryComparer Default { get; } = new(); 11 | 12 | public static int Compare(ReadOnlySpan x, ReadOnlySpan y) 13 | { 14 | if (x.Length < y.Length) 15 | return -1; 16 | 17 | if (x.Length > y.Length) 18 | return 1; 19 | 20 | for (int i = 0; i < x.Length; i++) 21 | { 22 | char xChar = char.ToUpperInvariant(x[i]); 23 | char yChar = char.ToUpperInvariant(y[i]); 24 | 25 | if (xChar < yChar) 26 | return -1; 27 | if (xChar > yChar) 28 | return 1; 29 | } 30 | 31 | return 0; 32 | } 33 | 34 | public int Compare(DirectoryEntry? x, DirectoryEntry? y) 35 | { 36 | Debug.Assert(x is not null && y is not null); 37 | 38 | if (x == null && y == null) 39 | return 0; 40 | 41 | if (x is null) 42 | return -1; 43 | 44 | if (y is null) 45 | return 1; 46 | 47 | return Compare(x.NameCharSpan, y.NameCharSpan); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /OpenMcdf/DirectoryEntryEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Enumerates instances from a . 7 | /// 8 | internal sealed class DirectoryEntryEnumerator : IEnumerator 9 | { 10 | private readonly DirectoryEntries directories; 11 | private bool start = true; 12 | private uint index = uint.MaxValue; 13 | private DirectoryEntry? current; 14 | 15 | public DirectoryEntryEnumerator(DirectoryEntries directories) 16 | { 17 | this.directories = directories; 18 | } 19 | 20 | /// 21 | public void Dispose() 22 | { 23 | } 24 | 25 | /// 26 | public DirectoryEntry Current 27 | { 28 | get 29 | { 30 | if (current is null) 31 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 32 | return current; 33 | } 34 | } 35 | 36 | /// 37 | object IEnumerator.Current => Current; 38 | 39 | /// 40 | public bool MoveNext() 41 | { 42 | if (start) 43 | { 44 | start = false; 45 | index = uint.MaxValue; 46 | } 47 | 48 | uint nextIndex = index + 1; 49 | if (!directories.TryGetDictionaryEntry(nextIndex, out current)) 50 | { 51 | index = uint.MaxValue; 52 | return false; 53 | } 54 | 55 | index = nextIndex; 56 | return true; 57 | } 58 | 59 | /// 60 | public void Reset() 61 | { 62 | start = true; 63 | current = null; 64 | index = uint.MaxValue; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /OpenMcdf/DirectoryTreeEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Enumerates the children of a . 7 | /// 8 | internal sealed class DirectoryTreeEnumerator : IEnumerator 9 | { 10 | private readonly DirectoryEntries directories; 11 | private readonly DirectoryEntry root; 12 | private readonly Stack stack = new(); 13 | DirectoryEntry? current; 14 | 15 | internal DirectoryTreeEnumerator(DirectoryEntries directories, DirectoryEntry root) 16 | { 17 | this.directories = directories; 18 | this.root = root; 19 | Reset(); 20 | } 21 | 22 | /// 23 | public void Dispose() 24 | { 25 | } 26 | 27 | /// 28 | public DirectoryEntry Current 29 | { 30 | get 31 | { 32 | if (current is null) 33 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 34 | return current; 35 | } 36 | } 37 | 38 | /// 39 | object IEnumerator.Current => Current; 40 | 41 | /// 42 | public bool MoveNext() 43 | { 44 | if (stack.Count == 0) 45 | { 46 | current = null; 47 | return false; 48 | } 49 | 50 | current = stack.Pop(); 51 | if (directories.TryGetDictionaryEntry(current.RightSiblingId, out DirectoryEntry? rightSibling)) 52 | PushLeft(rightSibling!); 53 | 54 | return true; 55 | } 56 | 57 | /// 58 | public void Reset() 59 | { 60 | current = null; 61 | stack.Clear(); 62 | if (root.ChildId != StreamId.NoStream) 63 | { 64 | DirectoryEntry child = directories.GetDictionaryEntry(root.ChildId); 65 | PushLeft(child); 66 | } 67 | } 68 | 69 | private void PushLeft(DirectoryEntry? node) 70 | { 71 | while (node is not null) 72 | { 73 | stack.Push(node); 74 | directories.TryGetDictionaryEntry(node.LeftSiblingId, out node); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /OpenMcdf/EntryInfo.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | public enum EntryType 4 | { 5 | Storage, 6 | Stream, 7 | } 8 | 9 | /// 10 | /// Encapsulates information about an entry in a . 11 | /// 12 | public readonly record struct EntryInfo( 13 | EntryType Type, 14 | string Path, 15 | string Name, 16 | long Length, 17 | Guid CLSID, 18 | DateTime CreationTime, 19 | DateTime ModifiedTime); 20 | -------------------------------------------------------------------------------- /OpenMcdf/FatEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Encapsulates an entry in the File Allocation Table (FAT). 7 | /// 8 | internal record struct FatEntry(uint Index, uint Value) 9 | { 10 | [ExcludeFromCodeCoverage] 11 | public override readonly string ToString() => $"#{Index}: {Value}"; 12 | } 13 | -------------------------------------------------------------------------------- /OpenMcdf/FatEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace OpenMcdf; 5 | 6 | /// 7 | /// Enumerates the records in a . 8 | /// 9 | internal class FatEnumerator : IEnumerator 10 | { 11 | readonly Fat fat; 12 | bool start = true; 13 | uint index = uint.MaxValue; 14 | uint value = uint.MaxValue; 15 | 16 | public FatEnumerator(Fat fat) 17 | { 18 | this.fat = fat; 19 | } 20 | 21 | /// 22 | public void Dispose() 23 | { 24 | } 25 | 26 | /// 27 | public FatEntry Current 28 | { 29 | get 30 | { 31 | if (index == uint.MaxValue) 32 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 33 | return new(index, value); 34 | } 35 | } 36 | 37 | /// 38 | object IEnumerator.Current => Current; 39 | 40 | /// 41 | public bool MoveNext() 42 | { 43 | if (start) 44 | { 45 | start = false; 46 | return MoveTo(0); 47 | } 48 | 49 | if (index >= SectorType.Maximum) 50 | return false; 51 | 52 | uint next = index + 1; 53 | return MoveTo(next); 54 | } 55 | 56 | public bool MoveTo(uint index) 57 | { 58 | ThrowHelper.ThrowIfSectorIdIsInvalid(index); 59 | 60 | start = false; 61 | if (this.index == index) 62 | return true; 63 | 64 | if (fat.TryGetValue(index, out value)) 65 | { 66 | if (value < SectorType.Maximum && value >= fat.Context.SectorCount) 67 | throw new FileFormatException($"FAT entry #{index} for sector {value} is beyond the end of the stream."); 68 | this.index = index; 69 | return true; 70 | } 71 | 72 | this.index = uint.MaxValue; 73 | return false; 74 | } 75 | 76 | public bool MoveNextFreeEntry() 77 | { 78 | while (MoveNext()) 79 | { 80 | if (value is SectorType.Free) 81 | return true; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | /// 88 | public void Reset() 89 | { 90 | start = true; 91 | index = uint.MaxValue; 92 | value = uint.MaxValue; 93 | } 94 | 95 | [ExcludeFromCodeCoverage] 96 | public override string ToString() => $"{Current}"; 97 | } 98 | -------------------------------------------------------------------------------- /OpenMcdf/FatSectorEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Enumerates the s of a compound file. 7 | /// 8 | internal sealed class FatSectorEnumerator : ContextBase, IEnumerator 9 | { 10 | private readonly DifatSectorEnumerator difatSectorEnumerator; 11 | private bool start = true; 12 | private uint index = uint.MaxValue; 13 | private Sector current = Sector.EndOfChain; 14 | 15 | public FatSectorEnumerator(RootContextSite rootContextSite) 16 | : base(rootContextSite) 17 | { 18 | difatSectorEnumerator = new(rootContextSite); 19 | } 20 | 21 | /// 22 | public void Dispose() 23 | { 24 | // Context is owned by a parent 25 | difatSectorEnumerator.Dispose(); 26 | } 27 | 28 | /// 29 | public Sector Current 30 | { 31 | get 32 | { 33 | if (current.Id == SectorType.EndOfChain) 34 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 35 | return current; 36 | } 37 | } 38 | 39 | /// 40 | object IEnumerator.Current => Current; 41 | 42 | /// 43 | public bool MoveNext() 44 | { 45 | if (start) 46 | { 47 | start = false; 48 | index = uint.MaxValue; 49 | } 50 | 51 | uint nextIndex = index + 1; 52 | return MoveTo(nextIndex); 53 | } 54 | 55 | /// 56 | /// Moves the enumerator to the specified sector. 57 | /// 58 | public bool MoveTo(uint index) 59 | { 60 | ThrowHelper.ThrowIfSectorIdIsInvalid(index); 61 | 62 | start = false; 63 | 64 | if (index == this.index) 65 | return true; 66 | 67 | if (index >= Context.Header.FatSectorCount) 68 | { 69 | this.index = uint.MaxValue; 70 | current = Sector.EndOfChain; 71 | return false; 72 | } 73 | 74 | if (index < Header.DifatArrayLength) 75 | { 76 | this.index = index; 77 | uint difatId = Context.Header.Difat[this.index]; 78 | current = new(difatId, Context.SectorSize); 79 | return true; 80 | } 81 | 82 | if (index < this.index) 83 | { 84 | this.index = Header.DifatArrayLength; 85 | } 86 | 87 | uint difatChainIndex = GetDifatChainIndexAndDifatEntryIndex(index, out long difatElementIndex); 88 | if (!difatSectorEnumerator.MoveTo(difatChainIndex)) 89 | { 90 | this.index = uint.MaxValue; 91 | current = Sector.EndOfChain; 92 | return false; 93 | } 94 | 95 | Sector difatSector = difatSectorEnumerator.Current; 96 | Context.Reader.Position = difatSector.Position + (difatElementIndex * sizeof(uint)); 97 | uint id = Context.Reader.ReadUInt32(); 98 | this.index = index; 99 | current = new Sector(id, Context.SectorSize); 100 | return true; 101 | } 102 | 103 | private uint GetDifatChainIndexAndDifatEntryIndex(uint index, out long difatElementIndex) 104 | => (uint)Math.DivRem(index - Header.DifatArrayLength, Context.DifatEntriesPerSector, out difatElementIndex); 105 | 106 | /// 107 | public void Reset() 108 | { 109 | start = true; 110 | index = uint.MaxValue; 111 | current = Sector.EndOfChain; 112 | difatSectorEnumerator.Reset(); 113 | } 114 | 115 | /// 116 | /// Extends the FAT by adding a new sector. 117 | /// 118 | /// The ID of the new sector that was added 119 | public uint Add() 120 | { 121 | Header header = Context.Header; 122 | uint nextIndex = Context.Header.FatSectorCount; 123 | Sector newFatSector = new(Context.SectorCount, Context.SectorSize); 124 | 125 | CfbBinaryWriter writer = Context.Writer; 126 | writer.Position = newFatSector.Position; 127 | writer.Write(SectorDataCache.GetFatEntryData(newFatSector.Length)); 128 | Context.ExtendStreamLength(newFatSector.EndPosition); 129 | 130 | header.FatSectorCount++; 131 | 132 | index = nextIndex; 133 | current = newFatSector; 134 | 135 | if (nextIndex < Header.DifatArrayLength) 136 | { 137 | header.Difat[nextIndex] = newFatSector.Id; 138 | } 139 | else 140 | { 141 | uint difatSectorIndex = GetDifatChainIndexAndDifatEntryIndex(nextIndex, out long difatElementIndex); 142 | if (!difatSectorEnumerator.MoveTo(difatSectorIndex)) 143 | difatSectorEnumerator.Add(); 144 | 145 | Sector difatSector = difatSectorEnumerator.Current; 146 | writer.Position = difatSector.Position + difatElementIndex * sizeof(uint); 147 | writer.Write(newFatSector.Id); 148 | } 149 | 150 | Context.Fat[newFatSector.Id] = SectorType.Fat; 151 | return newFatSector.Id; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /OpenMcdf/FileFormatException.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// The exception that is thrown when an compound file data stream contains invalid data. 5 | /// 6 | public class FileFormatException : FormatException 7 | { 8 | public FileFormatException() 9 | { 10 | } 11 | 12 | public FileFormatException(string message) : base(message) 13 | { 14 | } 15 | 16 | public FileFormatException(string message, Exception innerException) : base(message, innerException) 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /OpenMcdf/MiniFat.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Collections; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace OpenMcdf; 7 | 8 | /// 9 | /// Encapsulates getting and setting records in the mini FAT. 10 | /// 11 | internal sealed class MiniFat : ContextBase, IEnumerable, IDisposable 12 | { 13 | private readonly FatChainEnumerator fatChainEnumerator; 14 | private readonly int ElementsPerSector; 15 | private readonly byte[] cachedSectorBuffer; 16 | private bool isDirty; 17 | 18 | public MiniFat(RootContextSite rootContextSite) 19 | : base(rootContextSite) 20 | { 21 | ElementsPerSector = Context.SectorSize / sizeof(uint); 22 | fatChainEnumerator = new(Context.Fat, Context.Header.FirstMiniFatSectorId); 23 | cachedSectorBuffer = new byte[Context.SectorSize]; 24 | } 25 | 26 | public void Dispose() 27 | { 28 | Flush(); 29 | 30 | fatChainEnumerator.Dispose(); 31 | } 32 | 33 | public void Flush() 34 | { 35 | if (isDirty) 36 | { 37 | CfbBinaryWriter writer = Context.Writer; 38 | writer.Position = fatChainEnumerator.CurrentSector.Position; 39 | writer.Write(cachedSectorBuffer); 40 | isDirty = false; 41 | } 42 | } 43 | 44 | public IEnumerator GetEnumerator() => new MiniFatEnumerator(ContextSite); 45 | 46 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 47 | 48 | public uint this[uint key] 49 | { 50 | get 51 | { 52 | if (!TryGetValue(key, out uint value)) 53 | throw new FileFormatException($"Mini FAT index not found: {key}."); 54 | return value; 55 | } 56 | set 57 | { 58 | if (!TrySetValue(key, value)) 59 | throw new FileFormatException($"Mini FAT index not found: {key}."); 60 | } 61 | } 62 | 63 | bool TryMoveToSectorForKey(uint key, out long elementIndex) 64 | { 65 | uint fatChain = (uint)Math.DivRem(key, ElementsPerSector, out elementIndex); 66 | if (fatChainEnumerator.IsAt(fatChain)) 67 | return true; 68 | 69 | Flush(); 70 | 71 | bool ok = fatChainEnumerator.MoveTo(fatChain); 72 | if (!ok) 73 | return false; 74 | 75 | CfbBinaryReader reader = Context.Reader; 76 | reader.Position = fatChainEnumerator.CurrentSector.Position; 77 | reader.Read(cachedSectorBuffer, 0, cachedSectorBuffer.Length); 78 | return true; 79 | } 80 | 81 | public bool TryGetValue(uint key, out uint value) 82 | { 83 | ThrowHelper.ThrowIfSectorIdIsInvalid(key); 84 | 85 | if (!TryMoveToSectorForKey(key, out long elementIndex)) 86 | { 87 | value = uint.MaxValue; 88 | return false; 89 | } 90 | 91 | Span slice = cachedSectorBuffer.AsSpan((int)elementIndex * sizeof(uint)); 92 | value = BinaryPrimitives.ReadUInt32LittleEndian(slice); 93 | return true; 94 | } 95 | 96 | public bool TrySetValue(uint key, uint value) 97 | { 98 | ThrowHelper.ThrowIfSectorIdIsInvalid(key); 99 | 100 | if (!TryMoveToSectorForKey(key, out long elementIndex)) 101 | return false; 102 | 103 | Span slice = cachedSectorBuffer.AsSpan((int)elementIndex * sizeof(uint)); 104 | BinaryPrimitives.WriteUInt32LittleEndian(slice, value); 105 | isDirty = true; 106 | return true; 107 | } 108 | 109 | public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) 110 | { 111 | ThrowHelper.ThrowIfSectorIdIsInvalid(startIndex); 112 | 113 | bool movedToFreeEntry = miniFatEnumerator.MoveTo(startIndex) && miniFatEnumerator.MoveNextFreeEntry(); 114 | if (!movedToFreeEntry) 115 | { 116 | uint newSectorIndex = fatChainEnumerator.Extend(); 117 | Sector sector = new(newSectorIndex, Context.SectorSize); 118 | CfbBinaryWriter writer = Context.Writer; 119 | writer.Position = sector.Position; 120 | writer.Write(SectorDataCache.GetFatEntryData(sector.Length)); 121 | 122 | Header header = Context.Header; 123 | if (header.FirstMiniFatSectorId == SectorType.EndOfChain) 124 | header.FirstMiniFatSectorId = newSectorIndex; 125 | header.MiniFatSectorCount++; 126 | 127 | miniFatEnumerator.Reset(); // TODO: Jump closer to the new sector 128 | 129 | bool ok = miniFatEnumerator.MoveNextFreeEntry(); 130 | Debug.Assert(ok, "No free mini FAT entries found."); 131 | } 132 | 133 | FatEntry entry = miniFatEnumerator.Current; 134 | this[entry.Index] = SectorType.EndOfChain; 135 | 136 | Debug.Assert(entry.Value is SectorType.Free); 137 | MiniSector miniSector = new(entry.Index, Context.MiniSectorSize); 138 | if (Context.MiniStream.Length < miniSector.EndPosition) 139 | Context.MiniStream.SetLength(miniSector.EndPosition); 140 | 141 | return entry.Index; 142 | } 143 | 144 | [ExcludeFromCodeCoverage] 145 | internal void WriteTrace(TextWriter writer) 146 | { 147 | using MiniFatEnumerator miniFatEnumerator = new(ContextSite); 148 | 149 | writer.WriteLine("Start of Mini FAT ============"); 150 | while (miniFatEnumerator.MoveNext()) 151 | writer.WriteLine($"{miniFatEnumerator.Current}"); 152 | writer.WriteLine("End of Mini FAT =============="); 153 | } 154 | 155 | [ExcludeFromCodeCoverage] 156 | internal bool Validate() 157 | { 158 | using MiniFatEnumerator miniFatEnumerator = new(ContextSite); 159 | 160 | while (miniFatEnumerator.MoveNext()) 161 | { 162 | FatEntry current = miniFatEnumerator.Current; 163 | if (current.Value <= SectorType.Maximum && miniFatEnumerator.CurrentSector.EndPosition > Context.MiniStream.Length) 164 | { 165 | throw new FileFormatException($"Mini FAT entry {current} is beyond the end of the mini stream."); 166 | } 167 | } 168 | 169 | return true; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /OpenMcdf/MiniFatChainEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace OpenMcdf; 6 | 7 | /// 8 | /// Enumerates the s in a Mini FAT chain. 9 | /// 10 | internal sealed class MiniFatChainEnumerator : ContextBase, IEnumerator 11 | { 12 | private readonly MiniFatEnumerator miniFatEnumerator; 13 | private uint startId; 14 | private bool start = true; 15 | uint index = uint.MaxValue; 16 | private uint current = uint.MaxValue; 17 | private long length = -1; 18 | 19 | // Brent's cycle-finding algorithm 20 | private uint cycleLength = 1; 21 | private uint power = 1; 22 | private uint slow = uint.MaxValue; 23 | 24 | public MiniFatChainEnumerator(RootContextSite rootContextSite, uint startSectorId) 25 | : base(rootContextSite) 26 | { 27 | startId = startSectorId; 28 | miniFatEnumerator = new(rootContextSite); 29 | } 30 | 31 | /// 32 | public void Dispose() 33 | { 34 | miniFatEnumerator.Dispose(); 35 | } 36 | 37 | /// 38 | /// The index within the Mini FAT sector chain, or if the enumeration has not started. 39 | /// 40 | 41 | public MiniSector CurrentSector => new(Current, Context.MiniSectorSize); 42 | 43 | /// 44 | public uint Current 45 | { 46 | get 47 | { 48 | if (index == uint.MaxValue) 49 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 50 | return current; 51 | } 52 | } 53 | 54 | /// 55 | object IEnumerator.Current => Current; 56 | 57 | /// 58 | public bool MoveNext() 59 | { 60 | if (start) 61 | { 62 | start = false; 63 | index = 0; 64 | current = startId; 65 | } 66 | else if (!SectorType.IsFreeOrEndOfChain(current)) 67 | { 68 | uint value = Context.MiniFat[current]; 69 | if (value == SectorType.EndOfChain) 70 | { 71 | index = uint.MaxValue; 72 | current = uint.MaxValue; 73 | slow = uint.MaxValue; 74 | return false; 75 | } 76 | 77 | uint nextIndex = index + 1; 78 | if (nextIndex > SectorType.Maximum) 79 | throw new FileFormatException("Mini FAT chain length is greater than the maximum."); 80 | 81 | if (value == slow) 82 | throw new FileFormatException("Mini FAT chain contains a loop."); 83 | 84 | if (cycleLength == power) 85 | { 86 | cycleLength = 0; 87 | power *= 2; 88 | slow = value; 89 | } 90 | 91 | index = nextIndex; 92 | current = value; 93 | cycleLength++; 94 | return true; 95 | } 96 | 97 | if (SectorType.IsFreeOrEndOfChain(current)) 98 | { 99 | index = uint.MaxValue; 100 | current = uint.MaxValue; 101 | return false; 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /// 108 | /// Moves to the specified index within the mini FAT sector chain. 109 | /// 110 | /// 111 | /// true if the enumerator was successfully advanced to the given index 112 | public bool MoveTo(uint index) 113 | { 114 | if (index < this.index) 115 | Reset(); 116 | 117 | while (start || this.index < index) 118 | { 119 | if (!MoveNext()) 120 | return false; 121 | } 122 | 123 | return true; 124 | } 125 | 126 | public long GetLength() 127 | { 128 | if (length == -1) 129 | { 130 | Reset(); 131 | length = 0; 132 | while (MoveNext()) 133 | { 134 | length++; 135 | } 136 | } 137 | 138 | return length; 139 | } 140 | 141 | public uint Extend(uint requiredChainLength) 142 | { 143 | uint chainLength = (uint)GetLength(); 144 | if (chainLength >= requiredChainLength) 145 | throw new ArgumentException("The chain is already longer than required.", nameof(requiredChainLength)); 146 | 147 | if (startId == StreamId.NoStream) 148 | { 149 | startId = Context.MiniFat.Add(miniFatEnumerator, 0); 150 | chainLength = 1; 151 | } 152 | 153 | bool ok = MoveTo(chainLength - 1); 154 | Debug.Assert(ok); 155 | 156 | uint lastId = current; 157 | ok = miniFatEnumerator.MoveTo(lastId); 158 | Debug.Assert(ok); 159 | while (chainLength < requiredChainLength) 160 | { 161 | uint id = Context.MiniFat.Add(miniFatEnumerator, lastId); 162 | Context.MiniFat[lastId] = id; 163 | lastId = id; 164 | chainLength++; 165 | } 166 | 167 | #if DEBUG 168 | this.length = -1; 169 | this.length = GetLength(); 170 | Debug.Assert(length == requiredChainLength); 171 | #endif 172 | 173 | length = requiredChainLength; 174 | return startId; 175 | } 176 | 177 | public uint Shrink(uint requiredChainLength) 178 | { 179 | uint chainLength = (uint)GetLength(); 180 | if (chainLength <= requiredChainLength) 181 | throw new ArgumentException("The chain is already shorter than required.", nameof(requiredChainLength)); 182 | 183 | Reset(); 184 | 185 | uint lastId = current; 186 | while (MoveNext()) 187 | { 188 | if (lastId <= SectorType.Maximum) 189 | { 190 | if (index == requiredChainLength) 191 | Context.MiniFat[lastId] = SectorType.EndOfChain; 192 | else if (index > requiredChainLength) 193 | Context.MiniFat[lastId] = SectorType.Free; 194 | } 195 | 196 | lastId = current; 197 | } 198 | 199 | if (lastId <= SectorType.Maximum) 200 | Context.MiniFat[lastId] = SectorType.Free; 201 | 202 | if (requiredChainLength == 0) 203 | { 204 | startId = StreamId.NoStream; 205 | } 206 | 207 | #if DEBUG 208 | this.length = -1; 209 | this.length = GetLength(); 210 | Debug.Assert(length == requiredChainLength); 211 | #endif 212 | 213 | length = requiredChainLength; 214 | return startId; 215 | } 216 | 217 | /// 218 | public void Reset() 219 | { 220 | start = true; 221 | index = uint.MaxValue; 222 | current = uint.MaxValue; 223 | slow = uint.MaxValue; 224 | cycleLength = 1; 225 | power = 1; 226 | } 227 | 228 | [ExcludeFromCodeCoverage] 229 | public override string ToString() => $"Index: {index} Current: {current}"; 230 | } 231 | -------------------------------------------------------------------------------- /OpenMcdf/MiniFatEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Enumerates the s from the FAT chain for the mini FAT. 7 | /// 8 | internal sealed class MiniFatEnumerator : ContextBase, IEnumerator 9 | { 10 | private readonly FatChainEnumerator fatChainEnumerator; 11 | private bool start = true; 12 | private uint index = uint.MaxValue; 13 | private uint value = uint.MaxValue; 14 | 15 | public MiniFatEnumerator(RootContextSite rootContextSite) 16 | : base(rootContextSite) 17 | { 18 | fatChainEnumerator = new(Context.Fat, Context.Header.FirstMiniFatSectorId); 19 | } 20 | 21 | /// 22 | public void Dispose() 23 | { 24 | fatChainEnumerator.Dispose(); 25 | } 26 | 27 | public MiniSector CurrentSector 28 | { 29 | get 30 | { 31 | if (index == uint.MaxValue) 32 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 33 | return new(value, Context.MiniSectorSize); 34 | } 35 | } 36 | 37 | /// 38 | public FatEntry Current 39 | { 40 | get 41 | { 42 | if (index == uint.MaxValue) 43 | throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); 44 | return new(index, value); 45 | } 46 | } 47 | 48 | /// 49 | object IEnumerator.Current => Current; 50 | 51 | /// 52 | public bool MoveNext() 53 | { 54 | if (start) 55 | { 56 | start = false; 57 | return MoveTo(0); 58 | } 59 | 60 | if (index >= SectorType.Maximum) 61 | return false; 62 | 63 | uint next = index + 1; 64 | return MoveTo(next); 65 | } 66 | 67 | public bool MoveTo(uint index) 68 | { 69 | ThrowHelper.ThrowIfSectorIdIsInvalid(index); 70 | 71 | if (this.index == index) 72 | return true; 73 | 74 | if (Context.MiniFat.TryGetValue(index, out value)) 75 | { 76 | this.index = index; 77 | return true; 78 | } 79 | 80 | this.index = uint.MaxValue; 81 | return false; 82 | } 83 | 84 | public bool MoveNextFreeEntry() 85 | { 86 | while (MoveNext()) 87 | { 88 | if (value == SectorType.Free) 89 | { 90 | return true; 91 | } 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /// 98 | public void Reset() 99 | { 100 | fatChainEnumerator.Reset(Context.Header.FirstMiniFatSectorId); 101 | start = true; 102 | index = uint.MaxValue; 103 | value = uint.MaxValue; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /OpenMcdf/MiniSector.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Encapsulates information about a mini sector in a compound file. 7 | /// 8 | /// The ID of the mini sector 9 | /// The sector length 10 | internal record struct MiniSector(uint Id, int Length) 11 | { 12 | public readonly bool IsValid => Id <= SectorType.Maximum; 13 | 14 | /// 15 | /// The position of the mini sector in the mini FAT stream. 16 | /// 17 | public readonly long Position 18 | { 19 | get 20 | { 21 | ThrowIfInvalid(); 22 | return Id * Length; 23 | } 24 | } 25 | 26 | /// 27 | /// The end position of the mini sector in the mini FAT stream. 28 | /// 29 | public readonly long EndPosition 30 | { 31 | get 32 | { 33 | ThrowIfInvalid(); 34 | return (Id + 1) * Length; 35 | } 36 | } 37 | 38 | readonly void ThrowIfInvalid() 39 | { 40 | if (!IsValid) 41 | throw new InvalidOperationException($"Invalid mini FAT sector ID: {Id}."); 42 | } 43 | 44 | [ExcludeFromCodeCoverage] 45 | public override readonly string ToString() => $"{Id}"; 46 | } 47 | -------------------------------------------------------------------------------- /OpenMcdf/OpenMcdf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net8.0 5 | true 6 | true 7 | 8 | 9 | 10 | True 11 | OpenMcdf 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 17 | $(Version) 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /OpenMcdf/RootContextSite.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// A site for the object, to allow switching streams. 5 | /// 6 | internal class RootContextSite 7 | { 8 | RootContext? context; 9 | 10 | internal RootContext Context => context!; 11 | 12 | internal void Switch(RootContext context) 13 | { 14 | this.context = context; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /OpenMcdf/Sector.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Encapsulates information about a sector in a compound file. 7 | /// 8 | /// The sector ID 9 | /// The sector length 10 | internal record struct Sector(uint Id, int Length) 11 | { 12 | public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); 13 | 14 | public readonly bool IsValid => Id <= SectorType.Maximum; 15 | 16 | /// 17 | /// The position of the sector in the compound file stream. 18 | /// 19 | public readonly long Position 20 | { 21 | get 22 | { 23 | ThrowIfInvalid(); 24 | return (Id + 1) * Length; 25 | } 26 | } 27 | 28 | /// 29 | /// The end position of the sector in the compound file stream. 30 | /// 31 | public readonly long EndPosition 32 | { 33 | get 34 | { 35 | ThrowIfInvalid(); 36 | return (Id + 2) * Length; 37 | } 38 | } 39 | 40 | readonly void ThrowIfInvalid() 41 | { 42 | if (!IsValid) 43 | throw new InvalidOperationException($"Invalid FAT sector ID: {Id}."); 44 | } 45 | 46 | [ExcludeFromCodeCoverage] 47 | public override readonly string ToString() => $"{Id}"; 48 | } 49 | -------------------------------------------------------------------------------- /OpenMcdf/SectorDataCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace OpenMcdf; 5 | 6 | /// 7 | /// Caches data for adding new sectors to the FAT. 8 | /// 9 | internal static class SectorDataCache 10 | { 11 | static readonly ConcurrentDictionary freeFatSectorData = new(1, 2); 12 | 13 | public static byte[] GetFatEntryData(int sectorSize) 14 | { 15 | if (!freeFatSectorData.TryGetValue(sectorSize, out byte[]? data)) 16 | { 17 | data = new byte[sectorSize]; 18 | Span uintSpan = MemoryMarshal.Cast(data); 19 | uintSpan.Fill(SectorType.Free); 20 | freeFatSectorData.TryAdd(sectorSize, data); 21 | } 22 | 23 | return data; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /OpenMcdf/SectorType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// Defines the types of sectors in a compound file. 5 | /// 6 | internal static class SectorType 7 | { 8 | public const uint Maximum = 0xFFFFFFFA; 9 | public const uint Difat = 0xFFFFFFFC; // Specifies a DIFAT sector in the FAT. 10 | public const uint Fat = 0xFFFFFFFD; 11 | public const uint EndOfChain = 0xFFFFFFFE; 12 | public const uint Free = 0xFFFFFFFF; 13 | 14 | public static bool IsFreeOrEndOfChain(uint value) => value is Free or EndOfChain; 15 | } 16 | -------------------------------------------------------------------------------- /OpenMcdf/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenMcdf; 2 | 3 | /// 4 | /// Adds modern extension methods to the class for netstandard2.0. 5 | /// 6 | internal static class StreamExtensions 7 | { 8 | #if !NET7_0_OR_GREATER 9 | 10 | public static void ReadExactly(this Stream stream, byte[] buffer, int offset, int count) 11 | { 12 | if (count == 0) 13 | return; 14 | 15 | int totalRead = 0; 16 | do 17 | { 18 | int read = stream.Read(buffer, offset + totalRead, count - totalRead); 19 | if (read == 0) 20 | throw new EndOfStreamException(); 21 | 22 | totalRead += read; 23 | } while (totalRead < count); 24 | } 25 | 26 | #endif 27 | 28 | #if (!NETSTANDARD2_0 && !NETFRAMEWORK) 29 | 30 | public static int ReadByteCore(this Stream stream) 31 | { 32 | Span bytes = stackalloc byte[1]; 33 | int read = stream.Read(bytes); 34 | return read == 0 ? -1 : bytes[0]; 35 | } 36 | 37 | public static void WriteByteCore(this Stream stream, byte value) 38 | { 39 | stream.ThrowIfNotWritable(); 40 | 41 | ReadOnlySpan bytes = [value]; 42 | stream.Write(bytes); 43 | } 44 | 45 | #endif 46 | 47 | public static void CopyAllTo(this Stream source, Stream destination) 48 | { 49 | source.Position = 0; 50 | destination.Position = 0; 51 | destination.SetLength(source.Length); 52 | source.CopyTo(destination); 53 | destination.Position = 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /OpenMcdf/System/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_analyzer_diagnostic.severity = none -------------------------------------------------------------------------------- /OpenMcdf/System/CompilerServices.cs: -------------------------------------------------------------------------------- 1 | namespace System.Runtime.CompilerServices; 2 | 3 | internal class IsExternalInit 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /OpenMcdf/System/LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /OpenMcdf/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace OpenMcdf; 4 | 5 | /// 6 | /// Extensions to consistently throw exceptions. 7 | /// 8 | internal static class ThrowHelper 9 | { 10 | public static void ThrowIfDisposed(this object instance, bool disposed) 11 | { 12 | if (disposed) 13 | throw new ObjectDisposedException(instance.GetType().FullName); 14 | } 15 | 16 | public static void ThrowIfStreamArgumentsAreInvalid(byte[] buffer, int offset, int count) 17 | { 18 | if (buffer is null) 19 | throw new ArgumentNullException(nameof(buffer)); 20 | 21 | if (offset < 0) 22 | throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number."); 23 | 24 | if ((uint)count > buffer.Length - offset) 25 | throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); 26 | } 27 | 28 | public static void ThrowIfNotSeekable(this Stream stream) 29 | { 30 | if (!stream.CanSeek) 31 | throw new ArgumentException("Stream must be seekable", nameof(stream)); 32 | } 33 | 34 | public static void ThrowIfNotWritable(this Stream stream) 35 | { 36 | if (!stream.CanWrite) 37 | throw new NotSupportedException("Stream does not support writing."); 38 | } 39 | 40 | public static void ThrowSeekBeforeOrigin() => throw new IOException("An attempt was made to move the position before the beginning of the stream."); 41 | 42 | public static void ThrowIfNameIsInvalid(string value) 43 | { 44 | if (value is null) 45 | throw new ArgumentNullException(nameof(value)); 46 | 47 | if (value.Contains(@"\") || value.Contains(@"/") || value.Contains(@":") || value.Contains(@"!")) 48 | throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':', '!'.", nameof(value)); 49 | 50 | if (Encoding.Unicode.GetByteCount(value) > DirectoryEntry.NameFieldLength - 2) 51 | throw new ArgumentException($"{value} exceeds maximum encoded length of {DirectoryEntry.NameFieldLength} bytes.", nameof(value)); 52 | } 53 | 54 | public static void ThrowIfSectorIdIsInvalid(uint value) 55 | { 56 | if (value > SectorType.Maximum) 57 | throw new ArgumentOutOfRangeException(nameof(value), $"Invalid sector ID: {value:X8}."); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Actions](https://github.com/ironfede/openmcdf/actions/workflows/dotnet-desktop.yml/badge.svg) 2 | ![CodeQL](https://github.com/ironfede/openmcdf/actions/workflows/codeql.yml/badge.svg) 3 | [![OpenMcdf NuGet](https://img.shields.io/nuget/v/OpenMcdf?label=OpenMcdf%20NuGet)](https://www.nuget.org/packages/OpenMcdf) 4 | [![OpenMcdf.Ole NuGet](https://img.shields.io/nuget/vpre/OpenMcdf.Ole?label=OpenMcdf.Ole%20NuGet)](https://www.nuget.org/packages/OpenMcdf.Ole) 5 | [![NuGet Downloads](https://img.shields.io/nuget/dt/OpenMcdf)](https://www.nuget.org/packages/OpenMcdf) 6 | 7 | # OpenMcdf 8 | 9 | OpenMcdf is a fully .NET / C# library to manipulate [Compound File Binary File Format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) files, also known as [Structured Storage](https://learn.microsoft.com/en-us/windows/win32/stg/structured-storage-start-page). 10 | 11 | Compound files include multiple streams of information (document summary, user data) in a single container, and is used as the bases for many different file formats: 12 | - Microsoft Office (.doc, .xls, .ppt) 13 | - Windows thumbnails cache files (thumbs.db) 14 | - Outlook messages (.msg) 15 | - Visual Studio Solution Options (.suo) 16 | - Advanced Authoring Format (.aaf) 17 | 18 | OpenMcdf v3 has a rewritten API and supports: 19 | - An idiomatic dotnet API and exception hierarchy 20 | - Fast and efficient enumeration and manipulation of storages and streams 21 | - File sizes up to 16 TB (using major format version 4 with 4096 byte sectors) 22 | - Transactions (i.e. commit and/or revert) 23 | - Consolidation (i.e. reclamation of space by removing free sectors) 24 | - Nullable attributes 25 | 26 | Limitations: 27 | - No support for red-black tree balancing (directory entries are stored in a tree, but are not balanced. i.e. trees are "all-black") 28 | - No support for single writer, multiple readers 29 | 30 | ## Getting started 31 | 32 | To create a new compound file: 33 | 34 | ```C# 35 | byte[] b = new byte[10000]; 36 | 37 | using var root = RootStorage.Create("test.cfb"); 38 | using CfbStream stream = root.CreateStream("MyStream"); 39 | stream.Write(b, 0, b.Length); 40 | ``` 41 | 42 | To open an Excel workbook (.xls) and access its main data stream: 43 | 44 | ```C# 45 | using var root = RootStorage.OpenRead("report.xls"); 46 | using CfbStream workbookStream = root.OpenStream("Workbook"); 47 | ``` 48 | 49 | To create or delete storages and streams: 50 | 51 | ```C# 52 | using var root = RootStorage.Create("test.cfb"); 53 | root.CreateStorage("MyStorage"); 54 | root.CreateStream("MyStream"); 55 | root.Delete("MyStream"); 56 | ``` 57 | 58 | For transacted storages, changes can either be committed or reverted: 59 | 60 | ```C# 61 | using var root = RootStorage.Create("test.cfb", StorageModeFlags.Transacted); 62 | root.Commit(); 63 | // 64 | root.Revert(); 65 | ``` 66 | 67 | A root storage can be consolidated to reduce its on-disk size: 68 | 69 | ```C# 70 | root.Flush(consolidate: true); 71 | ``` 72 | 73 | ## Object Linking and Embedding (OLE) Property Set Data Structures 74 | 75 | Support for reading and writing [OLE Properties](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/bf7aeae8-c47a-4939-9f45-700158dac3bc) is available via the OpenMcdf.Ole package. However, ***the API is experimental and subject to change*** 76 | 77 | ```C# 78 | OlePropertiesContainer co = new(stream); 79 | foreach (OleProperty prop in co.Properties) 80 | { 81 | ... 82 | } 83 | ``` 84 | 85 | OpenMcdf runs happily on the [Mono](http://www.mono-project.com/) platform and multi-targets [**netstandard2.0**](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) and **net8.0** to maximize client compatibility and support modern dotnet features. 86 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0-preview1 2 | * Multi-targeting for netstandard2.0 and net8.0 3 | * Improved performance 4 | * Reduced memory usage 5 | * Idiomatic C# API 6 | * Idiomatic exception hierarchy 7 | * Nullable warnings and annotations 8 | * 16 TB files support 9 | * Transacted root storage support 10 | * Optional consolidation 11 | * Switching streams (i.e. save as) 12 | * Storage/stream full path discovery 13 | 14 | ## 2.2 15 | * ADD: NET Standard 2.0 platform support 16 | 17 | ## 2.1 18 | * Migration to GitHub 19 | * FIXED: Issues with failed initialization to FREESECT (0xFFFFFFFF) of FAT sectors 20 | * FIXED: Issues with file-corruption detection 21 | 22 | ## 2.0 - Stable release. 23 | * Last SourceForge release 24 | 25 | ## 2.0 pre-release (NOT FOR PRODUCTION) 26 | * ADD: Red-Black tree full implementation to speed up large data structure read access (thousands of stream/storage objects) 27 | * ADD: Enhanced Stream resizing 28 | * ADD: Extensions to use native .net framework Stream object 29 | * ADD: Code has been ported to .net 4.0 framework 30 | * NOTE: This is a technical preview not aimed to production use. 31 | 32 | ## 1.5.4 33 | * FIXED: In particular conditions, an opened file could be left opened after a loading exception 34 | * FIXED: Circular references of corrupted files could lead to stack overflows 35 | * FIXED: Enhanced standard compliance: corrupted file loading defaults to abort operation. 36 | * ADD: Version property 37 | * ADD: New overloaded constructors to force the load of possibly corrupted files. 38 | 39 | ## 1.5.3 40 | * ADD: 'GetAllNamedEntries' Method to access structured files without tree-loading performance penalties 41 | * ADD: New hex editor for structured storage explorer sample application 42 | 43 | ## 1.5.2 44 | * FIXED: Math error in sector number recognition caused exception when reading some streams 45 | * FIXED: Saving twice caused OutOfMemoryException 46 | * FIXED: Error when using names of exactly 31 characters for streams or storages 47 | 48 | ## 1.5.1. 49 | * FIXED: Casting error when removing uncommitted-added Stream. 50 | * ADDED: CFDuplicatedItem exception thrown when trying to add duplicated items (previously item addition was silently failing). 51 | 52 | ## 1.5.0 53 | * FIXED: Exception thrown when removing a stream of length equals to zero. 54 | 55 | ## 1.5.0 - RC1 56 | * ADD: New Update mode to commit changes to the underlying stream 57 | * ADD: Sector recycle to reuse unallocated sectors 58 | * ADD: File shrinking to compact compound files 59 | * ADD: Support for version 4 of specs (4096 bytes sectors) 60 | * ADD: Partial stream data reading to read data from a specified offset 61 | * ADD: Advanced lazy loading to reduce memory footprint of application 62 | * FIXED: CHANGED NAMESPACE to OpenMcdf 63 | 64 | ## 1.4.1 65 | * FIXED: ERROR, internal modifier applied to Delete method 66 | * FIXED: Redundant method call for DIFAT chain 67 | * ADD: 'Delete' feature for sample project 68 | 69 | ## 1.4.0 70 | * ADD: 'Remove' feature for storage and stream objects. 71 | * FIXED: ERROR in manipulation of streams with a length of 4096 bytes (cutoff bug) (Thx to meddingt) 72 | * FIXED: ERROR in zero sized streams 73 | 74 | ## 1.3.1 75 | * FIXED: Error in DIFAT sectors manipulation 76 | 77 | ## 1.3 78 | * FIXED: Null pointer in traversal with empty storages; 79 | 80 | ## 1.2 81 | * FIXED: Fixed ministream (<4096 bytes) bug; 82 | 83 | ## 1.1 84 | * ADD: Added traversal of Compound file method (VisitEntries); 85 | * FIXED: Fixed bug when multiple storage added; 86 | 87 | ## 1.0 88 | * Initial release 89 | -------------------------------------------------------------------------------- /StructuredStorage/LockBytes.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | using Windows.Win32; 4 | using Windows.Win32.Foundation; 5 | using Windows.Win32.System.Com.StructuredStorage; 6 | 7 | namespace StructuredStorage; 8 | 9 | /// 10 | /// Encapsulates ILockBytes over an HGlobal allocation. 11 | /// 12 | internal sealed class LockBytes : IDisposable 13 | { 14 | readonly ILockBytes lockBytes; 15 | private bool disposedValue; 16 | 17 | public LockBytes(int count) 18 | { 19 | IntPtr hGlobal = Marshal.AllocHGlobal(count); 20 | HRESULT hr = PInvoke.CreateILockBytesOnHGlobal((HGLOBAL)hGlobal, true, out lockBytes); 21 | hr.ThrowOnFailure(); 22 | } 23 | 24 | public LockBytes(MemoryStream stream) 25 | { 26 | var hGlobal = (HGLOBAL)Marshal.AllocHGlobal((int)stream.Length); 27 | Marshal.Copy(stream.GetBuffer(), 0, hGlobal, (int)stream.Length); 28 | HRESULT hr = PInvoke.CreateILockBytesOnHGlobal(hGlobal, true, out lockBytes); 29 | hr.ThrowOnFailure(); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | if (disposedValue) 35 | return; 36 | 37 | int count = Marshal.ReleaseComObject(lockBytes); 38 | Debug.Assert(count == 0); 39 | 40 | disposedValue = true; 41 | GC.SuppressFinalize(this); 42 | } 43 | 44 | ~LockBytes() 45 | { 46 | Dispose(); 47 | } 48 | 49 | internal ILockBytes ILockBytes => lockBytes; 50 | } 51 | -------------------------------------------------------------------------------- /StructuredStorage/NativeMethods.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/CsWin32.schema.json", 3 | "wideCharOnly": true, 4 | "emitSingleFile": true, 5 | "public": false 6 | } 7 | -------------------------------------------------------------------------------- /StructuredStorage/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | // Structured storage 2 | 3 | // Functions 4 | CreateILockBytesOnHGlobal 5 | PropVariantToVariant 6 | ReadClassStg 7 | ReadClassStm 8 | SHCreateItemFromParsingName 9 | SHCreateStreamOnFile 10 | StgCreateDocfileOnILockBytes 11 | StgCreateStorageEx 12 | StgIsStorageFile 13 | StgOpenStorage 14 | StgOpenStorageEx 15 | StgOpenStorageOnILockBytes 16 | VariantClear 17 | VariantToPropVariant 18 | WriteClassStg 19 | WriteClassStm 20 | 21 | // Interfaces 22 | IEnumSTATPROPSETSTG 23 | IEnumSTATPROPSTG 24 | IEnumSTATSTG 25 | ILockBytes 26 | IPropertySetStorage 27 | IPropertyStorage 28 | IRootStorage 29 | IStorage 30 | IStream 31 | 32 | // Enumerations 33 | PROPSETFLAG_* 34 | STGTY 35 | 36 | // Constants 37 | STG_E_* 38 | PROPSETFLAG_* 39 | -------------------------------------------------------------------------------- /StructuredStorage/PropertySetStorage.cs: -------------------------------------------------------------------------------- 1 | using Windows.Win32; 2 | using Windows.Win32.System.Com.StructuredStorage; 3 | 4 | namespace StructuredStorage; 5 | 6 | /// 7 | /// Wraps IPropertySetStorage. 8 | /// 9 | public sealed class PropertySetStorage 10 | { 11 | /// 12 | /// PROPSETFLAG constants. 13 | /// 14 | [Flags] 15 | #pragma warning disable CA1008 16 | public enum Flags 17 | { 18 | Default = (int)PInvoke.PROPSETFLAG_DEFAULT, 19 | NonSimple = (int)PInvoke.PROPSETFLAG_NONSIMPLE, 20 | ANSI = (int)PInvoke.PROPSETFLAG_ANSI, 21 | Unbuffered = (int)PInvoke.PROPSETFLAG_UNBUFFERED, 22 | CaseSensitive = (int)PInvoke.PROPSETFLAG_CASE_SENSITIVE, 23 | } 24 | #pragma warning restore CA1008 25 | 26 | private readonly IPropertySetStorage propSet; // Cast of IStorage does not need disposal 27 | 28 | internal PropertySetStorage(IStorage storage) 29 | { 30 | propSet = (IPropertySetStorage)storage; 31 | } 32 | 33 | public PropertyStorage Create(Guid formatID, StorageModes mode) => Create(formatID, Flags.Default, mode, Guid.Empty); 34 | 35 | public PropertyStorage Create(Guid formatID, Flags flags = Flags.Default, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) => Create(formatID, flags, mode, Guid.Empty); 36 | 37 | public unsafe PropertyStorage Create(Guid formatID, Flags flags, StorageModes mode, Guid classID) 38 | { 39 | propSet.Create(&formatID, &classID, (uint)flags, (uint)mode, out IPropertyStorage stg); 40 | return new(stg); 41 | } 42 | 43 | public unsafe PropertyStorage Open(Guid formatID, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) 44 | { 45 | propSet.Open(&formatID, (uint)mode, out IPropertyStorage propStorage); 46 | return new(propStorage); 47 | } 48 | 49 | public unsafe void Remove(Guid formatID) => propSet.Delete(&formatID); 50 | } 51 | -------------------------------------------------------------------------------- /StructuredStorage/PropertyStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using Windows.Win32; 5 | using Windows.Win32.Foundation; 6 | using Windows.Win32.System.Com.StructuredStorage; 7 | 8 | namespace StructuredStorage; 9 | 10 | /// 11 | /// Enumerates STATPROPSTG elements from a PropertyStorage. 12 | /// 13 | internal sealed class StatPropStgEnumerator : IEnumerator 14 | { 15 | readonly IEnumSTATPROPSTG enumerator; 16 | STATPROPSTG propStat; 17 | 18 | public STATPROPSTG Current => propStat; 19 | 20 | object IEnumerator.Current => propStat; 21 | 22 | public unsafe StatPropStgEnumerator(IPropertyStorage propertyStorage) 23 | { 24 | propertyStorage.Enum(out enumerator); 25 | } 26 | 27 | public unsafe void Dispose() 28 | { 29 | FreeName(); 30 | 31 | Marshal.ReleaseComObject(enumerator); 32 | } 33 | 34 | private unsafe void FreeName() 35 | { 36 | Marshal.FreeCoTaskMem((nint)propStat.lpwstrName.Value); 37 | propStat.lpwstrName = null; 38 | } 39 | 40 | public unsafe bool MoveNext() 41 | { 42 | FreeName(); 43 | 44 | fixed (STATPROPSTG* statPtr = &propStat) 45 | { 46 | uint fetched; 47 | enumerator.Next(1, statPtr, &fetched); 48 | return fetched > 0; 49 | } 50 | } 51 | 52 | public void Reset() 53 | { 54 | FreeName(); 55 | 56 | enumerator.Reset(); 57 | } 58 | } 59 | 60 | /// 61 | /// Creates an enumerator for STATPROPSTG elements from a PropertyStorage. 62 | /// 63 | internal sealed class StatPropStgCollection : IEnumerable 64 | { 65 | readonly IPropertyStorage propertyStorage; 66 | 67 | public StatPropStgCollection(IPropertyStorage propertyStorage) 68 | { 69 | this.propertyStorage = propertyStorage; 70 | } 71 | 72 | public IEnumerator GetEnumerator() => new StatPropStgEnumerator(propertyStorage); 73 | 74 | IEnumerator IEnumerable.GetEnumerator() => new StatPropStgEnumerator(propertyStorage); 75 | } 76 | 77 | /// 78 | /// Wraps IPropertyStorage. 79 | /// 80 | public sealed class PropertyStorage : IDisposable 81 | { 82 | private readonly IPropertyStorage propertyStorage; 83 | private bool disposed; 84 | 85 | internal unsafe PropertyStorage(IPropertyStorage propertyStorage) 86 | { 87 | this.propertyStorage = propertyStorage; 88 | StatPropStgCollection = new(propertyStorage); 89 | 90 | STATPROPSETSTG prop; 91 | this.propertyStorage.Stat(&prop); 92 | } 93 | 94 | #region IDisposable Members 95 | 96 | public void Dispose() 97 | { 98 | if (disposed) 99 | return; 100 | 101 | int count = Marshal.ReleaseComObject(propertyStorage); 102 | Debug.Assert(count == 0); 103 | 104 | disposed = true; 105 | } 106 | 107 | #endregion 108 | 109 | internal StatPropStgCollection StatPropStgCollection { get; } 110 | 111 | public void Flush(CommitFlags flags = CommitFlags.Default) => propertyStorage.Commit((uint)flags); 112 | 113 | public unsafe void Remove(int propertyID) 114 | { 115 | PROPSPEC propspec = new() 116 | { 117 | ulKind = PROPSPEC_KIND.PRSPEC_PROPID, 118 | Anonymous = new PROPSPEC._Anonymous_e__Union() 119 | { 120 | propid = (uint)propertyID, 121 | }, 122 | }; 123 | propertyStorage.DeleteMultiple(1, &propspec); 124 | } 125 | 126 | public void Revert() => propertyStorage.Revert(); 127 | 128 | public unsafe object? this[int propertyID] 129 | { 130 | get 131 | { 132 | PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID); 133 | 134 | var variants = new PROPVARIANT[1]; 135 | propertyStorage.ReadMultiple(1, &spec, variants); 136 | HRESULT hr = PInvoke.PropVariantToVariant(variants[0], out object variant); 137 | hr.ThrowOnFailure(); 138 | return variant; 139 | } 140 | 141 | set 142 | { 143 | PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID); 144 | 145 | HRESULT hr = PInvoke.VariantToPropVariant(value, out PROPVARIANT pv); 146 | hr.ThrowOnFailure(); 147 | 148 | PROPVARIANT[] pvs = [pv]; 149 | propertyStorage.WriteMultiple(1, &spec, pvs, 2); 150 | } 151 | } 152 | } 153 | 154 | static class PropVariantExtensions 155 | { 156 | public static PROPSPEC CreatePropSpec(PROPSPEC_KIND kind, int propertyID) 157 | { 158 | return new PROPSPEC 159 | { 160 | ulKind = kind, 161 | Anonymous = new PROPSPEC._Anonymous_e__Union 162 | { 163 | propid = (uint)propertyID, 164 | }, 165 | }; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /StructuredStorage/Stream.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | using Windows.Win32; 4 | using Windows.Win32.Foundation; 5 | using Windows.Win32.System.Com; 6 | 7 | namespace StructuredStorage; 8 | 9 | /// 10 | /// Implements Stream on an COM IStream. 11 | /// 12 | public sealed class Stream : System.IO.Stream 13 | { 14 | public Storage Parent { get; } 15 | 16 | readonly IStream stream; 17 | bool disposed; 18 | 19 | internal Stream(IStream stream, Storage parent) 20 | { 21 | this.stream = stream; 22 | Parent = parent; 23 | 24 | STGM mode = Stat.grfMode; 25 | CanRead = mode.HasFlag(STGM.STGM_READWRITE) || !mode.HasFlag(STGM.STGM_WRITE); 26 | CanWrite = mode.HasFlag(STGM.STGM_READWRITE) || mode.HasFlag(STGM.STGM_WRITE); 27 | } 28 | 29 | protected override void Dispose(bool disposing) 30 | { 31 | if (disposed) 32 | return; 33 | 34 | if (disposing) 35 | { 36 | Flush(); 37 | 38 | int count = Marshal.ReleaseComObject(stream); 39 | Debug.Assert(count == 0); 40 | } 41 | 42 | disposed = true; 43 | 44 | base.Dispose(disposing); 45 | } 46 | 47 | public override void Flush() => Flush(CommitFlags.Default); 48 | 49 | public void Flush(CommitFlags flags) 50 | { 51 | ObjectDisposedException.ThrowIf(disposed, this); 52 | 53 | stream.Commit((STGC)flags); 54 | } 55 | 56 | public override int Read(byte[] buffer, int offset, int count) 57 | { 58 | ObjectDisposedException.ThrowIf(disposed, this); 59 | 60 | Span slice = buffer.AsSpan(offset, count); 61 | return Read(slice); 62 | } 63 | 64 | public override unsafe int Read(Span buffer) 65 | { 66 | ObjectDisposedException.ThrowIf(disposed, this); 67 | 68 | fixed (byte* ptr = buffer) 69 | { 70 | uint read; 71 | HRESULT hr = stream.Read(ptr, (uint)buffer.Length, &read); 72 | hr.ThrowOnFailure(); 73 | return (int)read; 74 | } 75 | } 76 | 77 | public void Revert() 78 | { 79 | ObjectDisposedException.ThrowIf(disposed, this); 80 | 81 | stream.Revert(); 82 | } 83 | 84 | public override unsafe long Seek(long offset, SeekOrigin origin) 85 | { 86 | ObjectDisposedException.ThrowIf(disposed, this); 87 | 88 | ulong pos; 89 | stream.Seek(offset, origin, &pos); 90 | return (long)pos; 91 | } 92 | 93 | public override void SetLength(long value) 94 | { 95 | ObjectDisposedException.ThrowIf(disposed, this); 96 | 97 | stream.SetSize((ulong)value); 98 | } 99 | 100 | public override void Write(byte[] buffer, int offset, int count) 101 | { 102 | ObjectDisposedException.ThrowIf(disposed, this); 103 | 104 | ReadOnlySpan slice = buffer.AsSpan(offset, count); 105 | Write(slice); 106 | } 107 | 108 | public override unsafe void Write(ReadOnlySpan buffer) 109 | { 110 | ObjectDisposedException.ThrowIf(disposed, this); 111 | 112 | if (buffer.Length == 0) 113 | return; 114 | 115 | fixed (byte* ptr = buffer) 116 | { 117 | uint written; 118 | HRESULT result = stream.Write(ptr, (uint)buffer.Length, &written); 119 | result.ThrowOnFailure(); 120 | } 121 | } 122 | 123 | // Properties 124 | public override bool CanRead { get; } 125 | 126 | public override bool CanSeek => true; 127 | 128 | public override bool CanWrite { get; } 129 | 130 | public override long Length 131 | { 132 | get 133 | { 134 | ObjectDisposedException.ThrowIf(disposed, this); 135 | 136 | return (long)Stat.cbSize; 137 | } 138 | } 139 | 140 | public override unsafe long Position 141 | { 142 | get 143 | { 144 | ObjectDisposedException.ThrowIf(disposed, this); 145 | 146 | ulong pos; 147 | stream.Seek(0L, SeekOrigin.Current, &pos); 148 | return (long)pos; 149 | } 150 | 151 | set 152 | { 153 | ObjectDisposedException.ThrowIf(disposed, this); 154 | 155 | stream.Seek(value, SeekOrigin.Begin, null); 156 | } 157 | } 158 | 159 | public Guid Id 160 | { 161 | get 162 | { 163 | ObjectDisposedException.ThrowIf(disposed, this); 164 | 165 | HRESULT hr = PInvoke.ReadClassStm(stream, out Guid guid); 166 | hr.ThrowOnFailure(); 167 | return guid; 168 | } 169 | 170 | set 171 | { 172 | ObjectDisposedException.ThrowIf(disposed, this); 173 | 174 | HRESULT hr = PInvoke.WriteClassStm(stream, value); 175 | hr.ThrowOnFailure(); 176 | } 177 | } 178 | 179 | internal unsafe STATSTG Stat 180 | { 181 | get 182 | { 183 | STATSTG stat; 184 | stream.Stat(&stat, STATFLAG.STATFLAG_NONAME); 185 | return stat; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /StructuredStorage/StructuredStorage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0-windows 5 | true 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/OpenMcdf.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/OpenMcdf.snk -------------------------------------------------------------------------------- /StructuredStorageExplorer/PreferencesForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace StructuredStorageExplorer 2 | { 3 | partial class PreferencesForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.cbEnableValidation = new System.Windows.Forms.CheckBox(); 32 | this.groupBox1 = new System.Windows.Forms.GroupBox(); 33 | this.btnSavePreferences = new System.Windows.Forms.Button(); 34 | this.btnCancelPreferences = new System.Windows.Forms.Button(); 35 | this.groupBox1.SuspendLayout(); 36 | this.SuspendLayout(); 37 | // 38 | // cbEnableValidation 39 | // 40 | this.cbEnableValidation.AutoSize = true; 41 | this.cbEnableValidation.Location = new System.Drawing.Point(6, 25); 42 | this.cbEnableValidation.Name = "cbEnableValidation"; 43 | this.cbEnableValidation.Size = new System.Drawing.Size(277, 24); 44 | this.cbEnableValidation.TabIndex = 0; 45 | this.cbEnableValidation.Text = "File Validation Exceptions enabled"; 46 | this.cbEnableValidation.UseVisualStyleBackColor = true; 47 | // 48 | // groupBox1 49 | // 50 | this.groupBox1.Controls.Add(this.cbEnableValidation); 51 | this.groupBox1.Location = new System.Drawing.Point(12, 12); 52 | this.groupBox1.Name = "groupBox1"; 53 | this.groupBox1.Size = new System.Drawing.Size(555, 259); 54 | this.groupBox1.TabIndex = 1; 55 | this.groupBox1.TabStop = false; 56 | this.groupBox1.Text = "Preferences"; 57 | // 58 | // btnSavePreferences 59 | // 60 | this.btnSavePreferences.Location = new System.Drawing.Point(473, 298); 61 | this.btnSavePreferences.Name = "btnSavePreferences"; 62 | this.btnSavePreferences.Size = new System.Drawing.Size(94, 30); 63 | this.btnSavePreferences.TabIndex = 2; 64 | this.btnSavePreferences.Text = "OK"; 65 | this.btnSavePreferences.UseVisualStyleBackColor = true; 66 | this.btnSavePreferences.Click += new System.EventHandler(this.BtnSavePreferences_Click); 67 | // 68 | // btnCancelPreferences 69 | // 70 | this.btnCancelPreferences.Location = new System.Drawing.Point(373, 298); 71 | this.btnCancelPreferences.Name = "btnCancelPreferences"; 72 | this.btnCancelPreferences.Size = new System.Drawing.Size(94, 30); 73 | this.btnCancelPreferences.TabIndex = 3; 74 | this.btnCancelPreferences.Text = "Cancel"; 75 | this.btnCancelPreferences.UseVisualStyleBackColor = true; 76 | this.btnCancelPreferences.Click += new System.EventHandler(this.BtnCancelPreferences_Click); 77 | // 78 | // PreferencesForm 79 | // 80 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); 81 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 82 | this.ClientSize = new System.Drawing.Size(579, 340); 83 | this.Controls.Add(this.btnCancelPreferences); 84 | this.Controls.Add(this.btnSavePreferences); 85 | this.Controls.Add(this.groupBox1); 86 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; 87 | this.Name = "PreferencesForm"; 88 | this.ShowIcon = false; 89 | this.ShowInTaskbar = false; 90 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 91 | this.Text = "Preferences"; 92 | this.Load += new System.EventHandler(this.PreferencesForm_Load); 93 | this.groupBox1.ResumeLayout(false); 94 | this.groupBox1.PerformLayout(); 95 | this.ResumeLayout(false); 96 | 97 | } 98 | 99 | #endregion 100 | 101 | private System.Windows.Forms.CheckBox cbEnableValidation; 102 | private System.Windows.Forms.GroupBox groupBox1; 103 | private System.Windows.Forms.Button btnSavePreferences; 104 | private System.Windows.Forms.Button btnCancelPreferences; 105 | } 106 | } -------------------------------------------------------------------------------- /StructuredStorageExplorer/PreferencesForm.cs: -------------------------------------------------------------------------------- 1 | using StructuredStorageExplorer.Properties; 2 | 3 | namespace StructuredStorageExplorer; 4 | 5 | public partial class PreferencesForm : Form 6 | { 7 | public PreferencesForm() 8 | { 9 | InitializeComponent(); 10 | } 11 | 12 | private void BtnSavePreferences_Click(object sender, EventArgs e) 13 | { 14 | Settings.Default.EnableValidation = cbEnableValidation.Checked; 15 | Settings.Default.Save(); 16 | DialogResult = DialogResult.OK; 17 | Close(); 18 | } 19 | 20 | private void BtnCancelPreferences_Click(object sender, EventArgs e) 21 | { 22 | cbEnableValidation.Checked = Settings.Default.EnableValidation; 23 | DialogResult = DialogResult.Cancel; 24 | Close(); 25 | } 26 | 27 | private void PreferencesForm_Load(object sender, EventArgs e) 28 | { 29 | cbEnableValidation.Checked = Settings.Default.EnableValidation; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/PreferencesForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/Program.cs: -------------------------------------------------------------------------------- 1 | namespace StructuredStorageExplorer; 2 | 3 | static class Program 4 | { 5 | /// 6 | /// The main entry point for the application. 7 | /// 8 | [STAThread] 9 | static void Main() 10 | { 11 | Application.EnableVisualStyles(); 12 | Application.SetCompatibleTextRenderingDefault(false); 13 | Application.Run(new MainForm()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Il codice è stato generato da uno strumento. 4 | // Versione runtime:4.0.30319.42000 5 | // 6 | // Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se 7 | // il codice viene rigenerato. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace StructuredStorageExplorer.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// Classe di risorse fortemente tipizzata per la ricerca di stringhe localizzate e così via. 17 | /// 18 | // Questa classe è stata generata automaticamente dalla classe StronglyTypedResourceBuilder. 19 | // tramite uno strumento quale ResGen o Visual Studio. 20 | // Per aggiungere o rimuovere un membro, modificare il file con estensione ResX ed eseguire nuovamente ResGen 21 | // con l'opzione /str oppure ricompilare il progetto VS. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Restituisce l'istanza di ResourceManager nella cache utilizzata da questa classe. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("StructuredStorageExplorer.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Esegue l'override della proprietà CurrentUICulture del thread corrente per tutte le 51 | /// ricerche di risorse eseguite utilizzando questa classe di risorse fortemente tipizzata. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap disk { 67 | get { 68 | object obj = ResourceManager.GetObject("disk", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap door_out { 77 | get { 78 | object obj = ResourceManager.GetObject("door_out", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 85 | /// 86 | internal static System.Drawing.Bitmap folder { 87 | get { 88 | object obj = ResourceManager.GetObject("folder", resourceCulture); 89 | return ((System.Drawing.Bitmap)(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 95 | /// 96 | internal static System.Drawing.Bitmap page_white { 97 | get { 98 | object obj = ResourceManager.GetObject("page_white", resourceCulture); 99 | return ((System.Drawing.Bitmap)(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 105 | /// 106 | internal static System.Drawing.Bitmap storage { 107 | get { 108 | object obj = ResourceManager.GetObject("storage", resourceCulture); 109 | return ((System.Drawing.Bitmap)(obj)); 110 | } 111 | } 112 | 113 | /// 114 | /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. 115 | /// 116 | internal static System.Drawing.Bitmap stream { 117 | get { 118 | object obj = ResourceManager.GetObject("stream", resourceCulture); 119 | return ((System.Drawing.Bitmap)(obj)); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Il codice è stato generato da uno strumento. 4 | // Versione runtime:4.0.30319.42000 5 | // 6 | // Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se 7 | // il codice viene rigenerato. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace StructuredStorageExplorer.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.7.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 29 | public bool CommitEnabled { 30 | get { 31 | return ((bool)(this["CommitEnabled"])); 32 | } 33 | set { 34 | this["CommitEnabled"] = value; 35 | } 36 | } 37 | 38 | [global::System.Configuration.UserScopedSettingAttribute()] 39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 40 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 41 | public bool EnableValidation { 42 | get { 43 | return ((bool)(this["EnableValidation"])); 44 | } 45 | set { 46 | this["EnableValidation"] = value; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | True 7 | 8 | 9 | False 10 | 11 | 12 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/StreamDataProvider.cs: -------------------------------------------------------------------------------- 1 | using Be.Windows.Forms; 2 | using OpenMcdf; 3 | 4 | namespace StructuredStorageExplorer; 5 | 6 | internal sealed class StreamDataProvider : IByteProvider 7 | { 8 | /// 9 | /// Modifying stream 10 | /// 11 | readonly CfbStream _modifiedStream; 12 | 13 | /// 14 | /// Contains information about changes. 15 | /// 16 | bool _hasChanges; 17 | 18 | /// 19 | /// Initializes a new instance of the StreamDataProvider class. 20 | /// 21 | /// 22 | public StreamDataProvider(CfbStream modifiedStream) 23 | { 24 | byte[] data = new byte[modifiedStream.Length]; 25 | modifiedStream.Position = 0; 26 | modifiedStream.ReadExactly(data, 0, data.Length); 27 | Bytes = new ByteCollection(data); 28 | _modifiedStream = modifiedStream; 29 | } 30 | 31 | /// 32 | /// Raises the Changed event. 33 | /// 34 | void OnChanged(EventArgs e) 35 | { 36 | _hasChanges = true; 37 | 38 | Changed?.Invoke(this, e); 39 | } 40 | 41 | /// 42 | /// Raises the LengthChanged event. 43 | /// 44 | void OnLengthChanged(EventArgs e) 45 | { 46 | LengthChanged?.Invoke(this, e); 47 | } 48 | 49 | /// 50 | /// Gets the byte collection. 51 | /// 52 | public ByteCollection Bytes { get; } 53 | 54 | #region IByteProvider Members 55 | /// 56 | /// True, when changes are done. 57 | /// 58 | public bool HasChanges() 59 | { 60 | return _hasChanges; 61 | } 62 | 63 | /// 64 | /// Applies changes. 65 | /// 66 | public void ApplyChanges() 67 | { 68 | _hasChanges = false; 69 | 70 | _modifiedStream.Position = 0; 71 | _modifiedStream.Write(Bytes.ToArray()); 72 | } 73 | 74 | /// 75 | /// Occurs, when the write buffer contains new changes. 76 | /// 77 | public event EventHandler? Changed; 78 | 79 | /// 80 | /// Occurs, when InsertBytes or DeleteBytes method is called. 81 | /// 82 | public event EventHandler? LengthChanged; 83 | 84 | /// 85 | /// Reads a byte from the byte collection. 86 | /// 87 | /// the index of the byte to read 88 | /// the byte 89 | public byte ReadByte(long index) 90 | { return Bytes[(int)index]; } 91 | 92 | /// 93 | /// Write a byte into the byte collection. 94 | /// 95 | /// the index of the byte to write. 96 | /// the byte 97 | public void WriteByte(long index, byte value) 98 | { 99 | Bytes[(int)index] = value; 100 | OnChanged(EventArgs.Empty); 101 | } 102 | 103 | /// 104 | /// Deletes bytes from the byte collection. 105 | /// 106 | /// the start index of the bytes to delete. 107 | /// the length of bytes to delete. 108 | public void DeleteBytes(long index, long length) 109 | { 110 | int internal_index = (int)Math.Max(0, index); 111 | int internal_length = (int)Math.Min((int)Length, length); 112 | Bytes.RemoveRange(internal_index, internal_length); 113 | 114 | OnLengthChanged(EventArgs.Empty); 115 | OnChanged(EventArgs.Empty); 116 | } 117 | 118 | /// 119 | /// Inserts byte into the byte collection. 120 | /// 121 | /// the start index of the bytes in the byte collection 122 | /// the byte array to insert 123 | public void InsertBytes(long index, byte[] bs) 124 | { 125 | Bytes.InsertRange((int)index, bs); 126 | 127 | OnLengthChanged(EventArgs.Empty); 128 | OnChanged(EventArgs.Empty); 129 | } 130 | 131 | /// 132 | /// Gets the length of the bytes in the byte collection. 133 | /// 134 | public long Length => Bytes.Count; 135 | 136 | /// 137 | /// Returns true 138 | /// 139 | public bool SupportsWriteByte() 140 | { 141 | return true; 142 | } 143 | 144 | /// 145 | /// Returns true 146 | /// 147 | public bool SupportsInsertBytes() 148 | { 149 | return true; 150 | } 151 | 152 | /// 153 | /// Returns true 154 | /// 155 | public bool SupportsDeleteBytes() 156 | { 157 | return true; 158 | } 159 | #endregion 160 | } 161 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/StructuredStorageExplorer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0-windows 4 | WinExe 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/Utils.cs: -------------------------------------------------------------------------------- 1 | namespace StructuredStorageExplorer; 2 | 3 | static class Utils 4 | { 5 | public static DialogResult InputBox(string title, string promptText, ref string value) 6 | { 7 | using Form form = new(); 8 | using Label label = new(); 9 | using TextBox textBox = new(); 10 | using Button buttonOK = new(); 11 | using Button buttonCancel = new(); 12 | 13 | form.Text = title; 14 | label.Text = promptText; 15 | textBox.Text = value; 16 | 17 | buttonOK.Text = "OK"; 18 | buttonCancel.Text = "Cancel"; 19 | buttonOK.DialogResult = DialogResult.OK; 20 | buttonCancel.DialogResult = DialogResult.Cancel; 21 | 22 | label.SetBounds(9, 20, 372, 13); 23 | textBox.SetBounds(12, 36, 372, 20); 24 | buttonOK.SetBounds(228, 72, 75, 23); 25 | buttonCancel.SetBounds(309, 72, 75, 23); 26 | 27 | label.AutoSize = true; 28 | textBox.Anchor |= AnchorStyles.Right; 29 | buttonOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; 30 | buttonCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; 31 | 32 | form.ClientSize = new Size(396, 107); 33 | form.Controls.AddRange([label, textBox, buttonOK, buttonCancel]); 34 | form.ClientSize = new Size(Math.Max(300, label.Right + 10), form.ClientSize.Height); 35 | form.FormBorderStyle = FormBorderStyle.FixedDialog; 36 | form.StartPosition = FormStartPosition.CenterScreen; 37 | form.MinimizeBox = false; 38 | form.MaximizeBox = false; 39 | form.AcceptButton = buttonOK; 40 | form.CancelButton = buttonCancel; 41 | 42 | DialogResult dialogResult = form.ShowDialog(); 43 | value = textBox.Text; 44 | return dialogResult; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | True 12 | 13 | 14 | False 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/disk.png -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/door_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/door_out.png -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/folder.png -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/page_white.png -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/storage.png -------------------------------------------------------------------------------- /StructuredStorageExplorer/img/stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/StructuredStorageExplorer/img/stream.png -------------------------------------------------------------------------------- /docs/img/LOGO_OpenMcdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/LOGO_OpenMcdf.png -------------------------------------------------------------------------------- /docs/img/LOGO_OpenMcdf_H.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/LOGO_OpenMcdf_H.png -------------------------------------------------------------------------------- /docs/img/PartialStream.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/PartialStream.PNG -------------------------------------------------------------------------------- /docs/img/read_stream.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/read_stream.PNG -------------------------------------------------------------------------------- /docs/img/structured_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/structured_storage.png -------------------------------------------------------------------------------- /docs/img/visit_entries.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/visit_entries.PNG -------------------------------------------------------------------------------- /docs/img/write_stream.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/docs/img/write_stream.PNG -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | /*overflow:hidden;*/ 4 | position: relative; 5 | background-color: white; 6 | /*font-family: Segoe UI;*/ 7 | font-family: "wf_SegoeUILight","wf_SegoeUI","Segoe UI Light","Segoe WP Light","Segoe UI","Segoe","Segoe WP","Tahoma","Verdana","Arial","sans-serif"; 8 | /*font-weight: lighter;*/ 9 | font-size:14pt; 10 | text-align: center; 11 | margin: 0px; 12 | margin-top: 10px; 13 | padding: 0px; 14 | display: block; 15 | } 16 | 17 | li 18 | { 19 | margin-top: 3px; 20 | } 21 | 22 | div#container 23 | { 24 | margin-top: 0px; 25 | margin-left: auto; 26 | margin-bottom: 14px; 27 | margin-right: auto; 28 | width: 760px; 29 | text-align: center; 30 | } 31 | 32 | div#header 33 | { 34 | width: 780px; 35 | height: 120px; 36 | } 37 | 38 | div#main 39 | { 40 | width: 780px; /*border-color: green; border-width: 1px; border-style: solid;*/ 41 | text-align: justify; 42 | } 43 | 44 | div#footer 45 | { 46 | width: 780px; /*border-color: green; border-width: 1px; border-style: solid;*/ 47 | font-size: 8pt; 48 | text-align: right; 49 | } 50 | 51 | p 52 | { 53 | margin-bottom: 65px; 54 | } 55 | 56 | strong 57 | { 58 | font-weight: 600; 59 | } 60 | 61 | h1 62 | { 63 | font-size: 16pt; 64 | font-weight: 600; 65 | } 66 | 67 | h2 68 | { 69 | font-size: 16pt; 70 | font-weight: bold; 71 | } 72 | 73 | #header 74 | { 75 | width: 780px; 76 | height: 130px; /*border-color: red; border-width: 1px; border-style: solid;*/ 77 | } 78 | 79 | .modal_popup 80 | { 81 | 82 | position: absolute; 83 | z-index: 10000; 84 | 85 | } 86 | 87 | .popup_block 88 | { 89 | border:2px solid; 90 | border-color:Black; 91 | background-color: White; 92 | position:relative; 93 | z-index: 10000; 94 | padding: 30px; 95 | } 96 | 97 | .popup_footer 98 | { 99 | position: relative; 100 | background-color: White; 101 | z-index: 10000; 102 | padding-top:20px; 103 | bottom: 0px; 104 | } 105 | 106 | .fader 107 | { 108 | background: black; 109 | 110 | background: -webkit-gradient( 111 | linear, 112 | right top, 113 | left bottom, 114 | color-stop(0.18, rgb(5,3,0)), 115 | color-stop(0.59, rgb(148,147,143))); 116 | 117 | background: -moz-linear-gradient( 118 | right top, 119 | rgb(5,3,0) 18%, 120 | rgb(148,147,143) 59% 121 | ); 122 | 123 | position: fixed; 124 | width: 100%; 125 | height: 100%; 126 | filter: alpha(opacity=70); 127 | opacity: .70; 128 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; 129 | left: 0; 130 | top: 0; 131 | z-index: 10; 132 | } 133 | 134 | -------------------------------------------------------------------------------- /exclusion.dic: -------------------------------------------------------------------------------- 1 | Affero 2 | Blaseotto 3 | CFB 4 | CLSID 5 | codepage 6 | defragmentation 7 | depersist 8 | DIFAT 9 | dotnet 10 | endian 11 | enqueuing 12 | enum 13 | enums 14 | foreach 15 | Java 16 | Jeremy 17 | ironfede 18 | LINQ 19 | MCDF 20 | metadata 21 | namespace 22 | namespaces 23 | netstandard 24 | nullable 25 | OpenMcdf 26 | snupkg 27 | toolbar 28 | typelib 29 | Unicode 30 | versioned 31 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.402", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironfede/openmcdf/fea82254d5bc7c797bd5b8f3798e27033267ffc5/icon.png --------------------------------------------------------------------------------