From a640063dd42d4a855a6cd253a5e5166c082702e8 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 10 Oct 2024 13:37:14 +1300 Subject: [PATCH 001/114] Add .gitattributes and .gitignore. --- .gitattributes | 63 +++++++++ .gitignore | 363 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file From 54e3029cbfe80db66fd3b87e37c6f6433edb4a8a Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 10 Oct 2024 13:37:15 +1300 Subject: [PATCH 002/114] Add project files. --- .editorconfig | 372 +++++++++++++++++++++++++ .globalconfig | 186 +++++++++++++ OpenMcdf3.Tests/BinaryReaderTests.cs | 15 + OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 28 ++ OpenMcdf3.sln | 37 +++ OpenMcdf3/AssemblyInfo.cs | 19 ++ OpenMcdf3/McdfBinaryReader.cs | 16 ++ OpenMcdf3/McdfBinaryWriter.cs | 21 ++ OpenMcdf3/OpenMcdf3.csproj | 11 + OpenMcdf3/Storage.cs | 8 + 10 files changed, 713 insertions(+) create mode 100644 .editorconfig create mode 100644 .globalconfig create mode 100644 OpenMcdf3.Tests/BinaryReaderTests.cs create mode 100644 OpenMcdf3.Tests/OpenMcdf3.Tests.csproj create mode 100644 OpenMcdf3.sln create mode 100644 OpenMcdf3/AssemblyInfo.cs create mode 100644 OpenMcdf3/McdfBinaryReader.cs create mode 100644 OpenMcdf3/McdfBinaryWriter.cs create mode 100644 OpenMcdf3/OpenMcdf3.csproj create mode 100644 OpenMcdf3/Storage.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..faaf28ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,372 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +#### Visual Studio Spell Checker #### + +[*] +spelling_exclusion_path = .\ignored-words.txt + +#### VS Spell Checker #### + +[*] +vsspell_section_id = 57c612f5006c4c25a138fae34ef5a504 +vsspell_determine_resource_file_language_from_name = true +vsspell_additional_dictionary_folders_57c612f5006c4c25a138fae34ef5a504 = .\dictionaries +vsspell_ignored_words_57c612f5006c4c25a138fae34ef5a504 = clear_inherited|File:ignored-words.txt|Readit + +[*.{cs,da,de,el,es,fr,fi,it,ko,nb-NO,nl-BE,pl,pt,pt-BR,ru,sk,sv}.resx] +vsspell_spell_check_as_you_type = false +vsspell_include_in_project_spell_check = false + +[*.{Designer.cs,ai,crproj,csproj,props,runsettings,targets,vcxproj,wixproj,wxi,wxl,wxs,xaml,xml}] +vsspell_spell_check_as_you_type = false +vsspell_include_in_project_spell_check = false + +[Service References/*/*] +vsspell_spell_check_as_you_type = false +vsspell_include_in_project_spell_check = false + +[{ignored-words.txt,ControlWords.cs,LanguageIdTests.cs,Phoneme.cs,UnicodeData.txt}] +vsspell_spell_check_as_you_type = false +vsspell_include_in_project_spell_check = false + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 +trim_trailing_whitespace = true + +# New line preferences +end_of_line = crlf +insert_final_newline =true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:none +dotnet_style_qualification_for_field = false:none +dotnet_style_qualification_for_method = false:none +dotnet_style_qualification_for_property = false:none + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:none +dotnet_style_predefined_type_for_member_access = true:none + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:none + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:none +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:suggestion +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = true:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:none + +# Code-block preferences +csharp_prefer_braces = when_multiline:none +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:none + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +csharp_style_prefer_method_group_conversion = true:none +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:none +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:none +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:none +csharp_style_prefer_parameter_null_checking = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_top_level_statements = false:none +csharp_style_prefer_primary_constructors = true:suggestion + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none + +# IDE0005: Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = warning + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1121: Use built-in type alias +dotnet_diagnostic.SA1121.severity = none + +# SA1124: Do not use regions +dotnet_diagnostic.SA1124.severity = suggestion + +# SA1202: Elements should be ordered by access +dotnet_diagnostic.SA1202.severity = none + +# SA1400: Access modifier should be declared +dotnet_diagnostic.SA1400.severity = none + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# SA1404: Code analysis suppression should have justification +dotnet_diagnostic.SA1404.severity = none + +# SA1405: Debug.Assert should provide message text +dotnet_diagnostic.SA1405.severity = none + +# SA1407: Arithmetic expressions should declare precedence +dotnet_diagnostic.SA1407.severity = none + +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none + +# SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# S1104: Fields should not have public accessibility +dotnet_diagnostic.S1104.severity = none + +# S2344: Enumeration type names should not have "Flags" or "Enum" suffixes +dotnet_diagnostic.S2344.severity = none +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion + +[*.{config,crproj,csproj,props,pubxml,runsettings,settings,svcinfo,targets,vcxproj,wixproj,wxi,wxl,wxs,xml}] + +# Indentation and spacing +indent_size = 2 +indent_style = space +tab_width = 2 +trim_trailing_whitespace = true + +[*.xaml] + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 +trim_trailing_whitespace = true + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:none +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:none +dotnet_style_allow_multiple_blank_lines_experimental = true:none +dotnet_style_allow_statement_immediately_after_block_experimental = true:none +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:none +dotnet_style_qualification_for_field = false:none +dotnet_style_qualification_for_property = false:none +dotnet_style_qualification_for_method = false:none +dotnet_style_qualification_for_event = false:none +dotnet_style_prefer_collection_expression = when_types_exactly_match:suggestion + +[*.{hlsl}] + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 +trim_trailing_whitespace = true + +# Specific files +[{ort_inference.cc,ort_inference.h,pixel_classifier.h}] + +indent_size = 4 +indent_style = space +tab_width = 4 +trim_trailing_whitespace = true +cpp_indent_namespace_contents = false +cpp_indent_multi_line_relative_to = innermost_parenthesis +cpp_indent_within_parentheses = align_to_parenthesis +cpp_indent_preserve_within_parentheses = true +tab_width = 4 diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 00000000..8be6656a --- /dev/null +++ b/.globalconfig @@ -0,0 +1,186 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +dotnet_analyzer_diagnostic.category-Design.severity = warning +dotnet_analyzer_diagnostic.category-Interoperability.severity = suggestion +dotnet_analyzer_diagnostic.category-Maintainability.severity = warning +dotnet_analyzer_diagnostic.category-Naming.severity = warning +dotnet_analyzer_diagnostic.category-Performance.severity = warning +dotnet_analyzer_diagnostic.category-Reliability.severity = warning +dotnet_analyzer_diagnostic.category-Style.severity = warning + +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none + +# IDE0010: Populate switch +dotnet_diagnostic.IDE0010.severity = none + +# IDE0011: Add braces to 'if' statement +dotnet_diagnostic.IDE0011.severity = suggestion + +# IDE0022: Use expression body for method +dotnet_diagnostic.IDE0022.severity = suggestion + +# IDE0025: Use expression body for property +dotnet_diagnostic.IDE0025.severity = suggestion + +# IDE0028: Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028) +dotnet_diagnostic.IDE0028.severity = none + +# IDE0028: Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028) +dotnet_diagnostic.IDE0030.severity = suggestion + +# IDE0040: Accessibility modifiers required (disabled on build) +dotnet_diagnostic.IDE0040.severity = none + +# IDE0045: Use conditional expression for assignment +dotnet_diagnostic.IDE0045.severity = suggestion + +# IDE0046: Use conditional expression for return +dotnet_diagnostic.IDE0046.severity = none + +# IDE0047: Parentheses can be removed +dotnet_diagnostic.IDE0047.severity = suggestion + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0058: Expression value is never used +dotnet_diagnostic.IDE0058.severity = suggestion + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = suggestion + +# IDE0072: Populate switch +dotnet_diagnostic.IDE0072.severity = none + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = suggestion + +# IDE0251: Make member readonly +dotnet_diagnostic.IDE0251.severity = warning + +# IDE0270: Null check can be simplified +dotnet_diagnostic.IDE0270.severity = suggestion + +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = suggestion + +# IDE0300: Collection initialization can be simplified +dotnet_diagnostic.IDE0300.severity = suggestion + +# IDE0300: Collection initialization can be simplified (false positive on empty arrays) +dotnet_diagnostic.IDE0301.severity = suggestion + +# IDE0303: Collection initialization can be simplified +dotnet_diagnostic.IDE0302.severity = suggestion + +# IDE0303: Collection initialization can be simplified +dotnet_diagnostic.IDE0303.severity = suggestion + +# IDE0305: Collection initialization can be simplified +dotnet_diagnostic.IDE0305.severity = suggestion + +#CA1008: Enums should have zero value +dotnet_diagnostic.CA1008.severity = suggestion + +# CA1028: Enum storage should be Int32 +dotnet_diagnostic.CA1028.severity = suggestion + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = suggestion + +# CA1051: Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = none + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none + +# CA1304: Specify CultureInfo +dotnet_diagnostic.CA1304.severity = suggestion + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = suggestion + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = suggestion + +# CA1309: Use ordinal string comparison +dotnet_diagnostic.CA1309.severity = suggestion + +# CA1416: Validate platform compatibility +dotnet_diagnostic.CA1416.severity = none + +# CA1848: Use the LoggerMessage delegates +dotnet_diagnostic.CA1848.severity = none + +# CA1711: Identifiers should not have incorrect suffix +dotnet_code_quality.CA1711.allowed_suffixes = Flag|Flags + +# CA1721: Property names should not match get methods +dotnet_diagnostic.CA1721.severity = suggestion + +# CA1724: Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = suggestion + +# CA1814: Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = suggestion + +# CA1826: Use property instead of Linq Enumerable method +dotnet_code_quality.CA1826.exclude_ordefault_methods = true + +# CA1863: Use 'CompositeFormat' +dotnet_diagnostic.CA1863.severity = none + +# CA2007: Do not directly await a Task +dotnet_diagnostic.CA2007.severity = suggestion + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = suggestion + +# CA2109: Review visible event handlers +dotnet_diagnostic.CA2109.severity = none + +# CA2229: Implement serialization constructors +dotnet_diagnostic.CA2229.severity = none + +# CA2300: Do not use insecure deserializer BinaryFormatter +dotnet_diagnostic.CA2300.severity = none + +# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +dotnet_diagnostic.CA2302.severity = suggestion + +# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5392.severity = suggestion + +# CA5393: Do not use unsafe DllImportSearchPath value +dotnet_diagnostic.CA5393.severity = none + +# CA5394: Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = suggestion + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none + +# MSTEST0015: Test method should not be ignored +dotnet_diagnostic.MSTEST0015.severity = none + +# SYSLIB0011: Type or member is obsolete +dotnet_diagnostic.SYSLIB0011.severity = none + +# SA0001: XML comment analysis disabled +dotnet_diagnostic.SA0001.severity = none + +# SA1215: An instance readonly element is positioned beneath an instance non-readonly element of the same type +dotnet_diagnostic.SA1215.severity = warning + +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = suggestion + +# SA1628: Documentation text should begin with a capital letter +dotnet_diagnostic.SA1628.severity = warning + +# SA1201: Elements must appear in the correct order +dotnet_diagnostic.SA1201.severity = suggestion + +# SYSLIB1045: Use GeneratedRegexAttribute to generate the regular expression implementation at compile time +dotnet_diagnostic.SYSLIB1045.severity = suggestion diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf3.Tests/BinaryReaderTests.cs new file mode 100644 index 00000000..0a687fed --- /dev/null +++ b/OpenMcdf3.Tests/BinaryReaderTests.cs @@ -0,0 +1,15 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public class BinaryReaderTests +{ + [TestMethod] + public void Guid() + { + byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; + using MemoryStream stream = new(bytes); + using McdfBinaryReader reader = new(stream); + Guid guid = reader.ReadGuid(); + Assert.AreEqual(new Guid(bytes), guid); + } +} \ No newline at end of file diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj new file mode 100644 index 00000000..adc3c029 --- /dev/null +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + 11.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln new file mode 100644 index 00000000..5999bf3c --- /dev/null +++ b/OpenMcdf3.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3", "D:\OpenMcdf3\OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3.Tests", "D:\OpenMcdf3\OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34030FA7-0A06-43D1-85DD-ADD39D502C3C}" + ProjectSection(SolutionItems) = preProject + D:\OpenMcdf3\.editorconfig = D:\OpenMcdf3\.editorconfig + D:\OpenMcdf3\.globalconfig = D:\OpenMcdf3\.globalconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B90DDE7E-803A-4890-82F0-09DAD0FF66D8}.Release|Any CPU.Build.0 = Release|Any CPU + {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {927A4DA2-8926-4AC2-A48C-976978B5F2AA} + EndGlobalSection +EndGlobal diff --git a/OpenMcdf3/AssemblyInfo.cs b/OpenMcdf3/AssemblyInfo.cs new file mode 100644 index 00000000..c16894d6 --- /dev/null +++ b/OpenMcdf3/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("a96ebb34-8c16-4c7e-b9f7-651ba754b722")] +[assembly: InternalsVisibleTo("OpenMcdf3.Tests")] diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs new file mode 100644 index 00000000..87a54a07 --- /dev/null +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -0,0 +1,16 @@ +namespace OpenMcdf3; + +internal class McdfBinaryReader : BinaryReader +{ + public McdfBinaryReader(Stream input) : base(input) + { + } + + public Guid ReadGuid() => new(ReadBytes(16)); + + public DateTime ReadFileTime() + { + long fileTime = ReadInt64(); + return DateTime.FromFileTimeUtc(fileTime); + } +} diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs new file mode 100644 index 00000000..89743606 --- /dev/null +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -0,0 +1,21 @@ +namespace OpenMcdf3; + +internal class McdfBinaryWriter : BinaryWriter +{ + public McdfBinaryWriter(Stream input) : base(input) + { + } + + public void Write(Guid value) + { + // TODO: Avoid heap allocation + byte[] bytes = value.ToByteArray(); + Write(bytes, 0, bytes.Length); + } + + public void Write(DateTime value) + { + long fileTime = value.ToFileTimeUtc(); + Write(fileTime); + } +} diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj new file mode 100644 index 00000000..1ace06ea --- /dev/null +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + 11.0 + enable + enable + true + + + diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs new file mode 100644 index 00000000..6ace1d1a --- /dev/null +++ b/OpenMcdf3/Storage.cs @@ -0,0 +1,8 @@ +namespace OpenMcdf3; + +public class Storage +{ + public static void Open(string fileName) + { + } +} From 1e7ff7f08101e6b40ae13e5ed033fd5254273973 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 10 Oct 2024 15:40:40 +1300 Subject: [PATCH 003/114] Read/write headers --- OpenMcdf3.Tests/BinaryReaderTests.cs | 4 +- OpenMcdf3.Tests/HeaderTests.cs | 13 +++++ OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 18 ++++-- OpenMcdf3.Tests/_Test.ppt | Bin 0 -> 174592 bytes OpenMcdf3/Header.cs | 77 +++++++++++++++++++++++++ OpenMcdf3/McdfBinaryReader.cs | 39 +++++++++++++ OpenMcdf3/McdfBinaryWriter.cs | 28 ++++++++- OpenMcdf3/RootStorage.cs | 38 ++++++++++++ OpenMcdf3/Sector.cs | 6 ++ OpenMcdf3/SectorType.cs | 10 ++++ OpenMcdf3/Storage.cs | 3 - 11 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 OpenMcdf3.Tests/HeaderTests.cs create mode 100644 OpenMcdf3.Tests/_Test.ppt create mode 100644 OpenMcdf3/Header.cs create mode 100644 OpenMcdf3/RootStorage.cs create mode 100644 OpenMcdf3/Sector.cs create mode 100644 OpenMcdf3/SectorType.cs diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf3.Tests/BinaryReaderTests.cs index 0a687fed..34f06412 100644 --- a/OpenMcdf3.Tests/BinaryReaderTests.cs +++ b/OpenMcdf3.Tests/BinaryReaderTests.cs @@ -1,7 +1,7 @@ namespace OpenMcdf3.Tests; [TestClass] -public class BinaryReaderTests +public sealed class BinaryReaderTests { [TestMethod] public void Guid() @@ -12,4 +12,4 @@ public void Guid() Guid guid = reader.ReadGuid(); Assert.AreEqual(new Guid(bytes), guid); } -} \ No newline at end of file +} diff --git a/OpenMcdf3.Tests/HeaderTests.cs b/OpenMcdf3.Tests/HeaderTests.cs new file mode 100644 index 00000000..3968ee36 --- /dev/null +++ b/OpenMcdf3.Tests/HeaderTests.cs @@ -0,0 +1,13 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class HeaderTests +{ + [TestMethod] + public void Header() + { + using FileStream stream = File.OpenRead("_Test.ppt"); + using McdfBinaryReader reader = new(stream); + Header header = reader.ReadHeader(); + } +} diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index adc3c029..664cdf8c 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -1,20 +1,20 @@ - netstandard2.0 - 11.0 + net8.0 + Exe + 11.0 enable enable false true + true - - - - + + @@ -25,4 +25,10 @@ + + + PreserveNewest + + + diff --git a/OpenMcdf3.Tests/_Test.ppt b/OpenMcdf3.Tests/_Test.ppt new file mode 100644 index 0000000000000000000000000000000000000000..4b5a7692c487ed3675640407315cdb4240fdb68a GIT binary patch literal 174592 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 sTd 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 literal 0 HcmV?d00001 diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs new file mode 100644 index 00000000..0ab79724 --- /dev/null +++ b/OpenMcdf3/Header.cs @@ -0,0 +1,77 @@ +namespace OpenMcdf3; + + +internal sealed class Header +{ + internal const int DifatLength = 109; + internal const ushort LittleEndian = 0xFFFE; + internal const ushort SectorShiftV3 = 0x0009; + internal const ushort SectorShiftV4 = 0x000C; + internal const short MiniSectorShift = 6; + internal const uint MiniStreamCutoffSize = 4096; + + internal static readonly byte[] Signature = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; + + internal static readonly byte[] Unused = new byte[6]; + private ushort majorVersion; + private ushort sectorShift = SectorShiftV3; + + public Guid CLSID { get; set; } + + public ushort MinorVersion { get; set; } + + public ushort MajorVersion + { + get => majorVersion; set + { + if (value is not 3 and not 4) + throw new FormatException($"Unsupported major version: {value}. Only 3 and 4 are supported"); + majorVersion = value; + } + } + + public ushort SectorShift + { + get => sectorShift; set + { + if (MajorVersion == 3 && value != SectorShiftV3) + throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV3:X4} is supported for Major Version 3"); + if (MajorVersion == 4 && value != SectorShiftV4) + throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV4:X4} is supported for Major Version 4"); + + sectorShift = value; + } + } + + public uint DirectorySectorCount { get; set; } + + public uint FatSectorCount { get; set; } + + public uint FirstDirectorySectorID { get; set; } = (uint)SectorType.EndOfChain; + + public uint TransactionSignature { get; set; } + + ///

+ /// This integer field contains the starting sector number for the mini FAT + /// + public uint FirstMiniFatSectorID { get; set; } = (uint)SectorType.EndOfChain; + + public uint MiniFatSectorCount { get; set; } + + public uint FirstDifatSectorID { get; set; } = (uint)SectorType.EndOfChain; + + public uint DifatSectorCount { get; set; } + + public uint[] Difat { get; } = new uint[DifatLength]; + + public int SectorSize => 1 << SectorShift; + + public Header(Version version = Version.V3) + { + MajorVersion = (ushort)version; + for (int i = 0; i < Difat.Length; i++) + { + Difat[i] = (uint)SectorType.Free; + } + } +} diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index 87a54a07..2f98116c 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -13,4 +13,43 @@ public DateTime ReadFileTime() long fileTime = ReadInt64(); return DateTime.FromFileTimeUtc(fileTime); } + + private void ReadBytes(byte[] buffer) => Read(buffer, 0, buffer.Length); + + public Header ReadHeader() + { + Header header = new(); + Read(buffer, 0, Header.Signature.Length); + if (!buffer.Take(Header.Signature.Length).SequenceEqual(Header.Signature)) + throw new FormatException("Invalid signature"); + header.CLSID = ReadGuid(); + header.MinorVersion = ReadUInt16(); + header.MajorVersion = ReadUInt16(); + ushort byteOrder = ReadUInt16(); + if (byteOrder != Header.LittleEndian) + throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})"); + header.SectorShift = ReadUInt16(); + ushort miniSectorShift = ReadUInt16(); + if (miniSectorShift != Header.MiniSectorShift) + throw new FormatException($"Unsupported sector shift {miniSectorShift}. Only {Header.MiniSectorShift} is supported"); + this.FillBuffer(6); + header.DirectorySectorCount = ReadUInt32(); + header.FatSectorCount = ReadUInt32(); + header.FirstDirectorySectorID = ReadUInt32(); + this.FillBuffer(4); + uint miniStreamCutoffSize = ReadUInt32(); + if (miniStreamCutoffSize != Header.MiniStreamCutoffSize) + throw new FormatException("Mini stream cutoff size must be 4096 byte"); + header.FirstMiniFatSectorID = ReadUInt32(); + header.MiniFatSectorCount = ReadUInt32(); + header.FirstDifatSectorID = ReadUInt32(); + header.DifatSectorCount = ReadUInt32(); + + for (int i = 0; i < Header.DifatLength; i++) + { + header.Difat[i] = ReadUInt32(); + } + + return header; + } } diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index 89743606..e9befdf0 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -1,4 +1,7 @@ -namespace OpenMcdf3; +using System.IO; +using System.Security.Claims; + +namespace OpenMcdf3; internal class McdfBinaryWriter : BinaryWriter { @@ -18,4 +21,27 @@ public void Write(DateTime value) long fileTime = value.ToFileTimeUtc(); Write(fileTime); } + + private void WriteBytes(byte[] buffer) => Write(buffer, 0, buffer.Length); + + public void Write(Header header) + { + Write(Header.Signature); + Write(header.CLSID); + Write(header.MinorVersion); + Write(header.MajorVersion); + Write(Header.LittleEndian); + Write(header.SectorShift); + Write(Header.MiniSectorShift); + WriteBytes(Header.Unused); + Write(header.DirectorySectorCount); + Write(header.FatSectorCount); + Write(header.FirstDirectorySectorID); + Write((uint)0); + Write(Header.MiniStreamCutoffSize); + Write(header.FirstMiniFatSectorID); + Write(header.MiniFatSectorCount); + Write(header.FirstDifatSectorID); + Write(header.DifatSectorCount); + } } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs new file mode 100644 index 00000000..c1c1b2b6 --- /dev/null +++ b/OpenMcdf3/RootStorage.cs @@ -0,0 +1,38 @@ +namespace OpenMcdf3; + +public enum Version : ushort +{ + V3 = 3, + V4 = 4 +} + +public sealed class RootStorage : Storage +{ + readonly Header header; + readonly McdfBinaryReader reader; + readonly McdfBinaryWriter? writer; + + RootStorage(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) + { + this.header = header; + this.reader = reader; + this.writer = writer; + } + + public static RootStorage Create(string fileName, Version version = Version.V3) + { + FileStream stream = File.Create(fileName); + Header header = new(version); + McdfBinaryReader reader = new(stream); + McdfBinaryWriter writer = new(stream); + return new RootStorage(header, reader, writer); + } + + public static RootStorage Open(string fileName, FileMode mode) + { + FileStream stream = File.Open(fileName, mode); + McdfBinaryReader reader = new(stream); + Header header = reader.ReadHeader(); + return new RootStorage(header, reader); + } +} diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs new file mode 100644 index 00000000..18b27bb2 --- /dev/null +++ b/OpenMcdf3/Sector.cs @@ -0,0 +1,6 @@ +namespace OpenMcdf3; + +internal class Sector +{ + const int MiniSectorSize = 64; +} diff --git a/OpenMcdf3/SectorType.cs b/OpenMcdf3/SectorType.cs new file mode 100644 index 00000000..f38feef9 --- /dev/null +++ b/OpenMcdf3/SectorType.cs @@ -0,0 +1,10 @@ +namespace OpenMcdf3; + +internal enum SectorType : uint +{ + Maximum = 0xFFFFFFFA, + Difat = 0xFFFFFFFC, // Specifies a DIFAT sector in the FAT. + Fat = 0xFFFFFFFD, + EndOfChain = 0xFFFFFFFE, + Free = 0xFFFFFFFF, +} diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 6ace1d1a..8bc4cfe4 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -2,7 +2,4 @@ public class Storage { - public static void Open(string fileName) - { - } } From 43e329272847c803b71ad95edef135977d626cee Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 10 Oct 2024 17:00:46 +1300 Subject: [PATCH 004/114] Read/write directory entries --- OpenMcdf3.Tests/BinaryReaderTests.cs | 20 ++++++- OpenMcdf3.Tests/HeaderTests.cs | 13 ----- OpenMcdf3/DirectoryEntry.cs | 82 ++++++++++++++++++++++++++++ OpenMcdf3/McdfBinaryReader.cs | 45 ++++++++++++++- OpenMcdf3/McdfBinaryWriter.cs | 23 +++++++- OpenMcdf3/Storage.cs | 10 ++++ 6 files changed, 176 insertions(+), 17 deletions(-) delete mode 100644 OpenMcdf3.Tests/HeaderTests.cs create mode 100644 OpenMcdf3/DirectoryEntry.cs diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf3.Tests/BinaryReaderTests.cs index 34f06412..4e570624 100644 --- a/OpenMcdf3.Tests/BinaryReaderTests.cs +++ b/OpenMcdf3.Tests/BinaryReaderTests.cs @@ -4,7 +4,7 @@ namespace OpenMcdf3.Tests; public sealed class BinaryReaderTests { [TestMethod] - public void Guid() + public void ReadGuid() { byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; using MemoryStream stream = new(bytes); @@ -12,4 +12,22 @@ public void Guid() Guid guid = reader.ReadGuid(); Assert.AreEqual(new Guid(bytes), guid); } + + [TestMethod] + public void ReadFileTime() + { + byte[] bytes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + using MemoryStream stream = new(bytes); + using McdfBinaryReader reader = new(stream); + DateTime actual = reader.ReadFileTime(); + Assert.AreEqual(DirectoryEntry.ZeroFileTime, actual); + } + + [TestMethod] + public void ReadHeader() + { + using FileStream stream = File.OpenRead("_Test.ppt"); + using McdfBinaryReader reader = new(stream); + Header header = reader.ReadHeader(); + } } diff --git a/OpenMcdf3.Tests/HeaderTests.cs b/OpenMcdf3.Tests/HeaderTests.cs deleted file mode 100644 index 3968ee36..00000000 --- a/OpenMcdf3.Tests/HeaderTests.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OpenMcdf3.Tests; - -[TestClass] -public sealed class HeaderTests -{ - [TestMethod] - public void Header() - { - using FileStream stream = File.OpenRead("_Test.ppt"); - using McdfBinaryReader reader = new(stream); - Header header = reader.ReadHeader(); - } -} diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs new file mode 100644 index 00000000..45ae5a19 --- /dev/null +++ b/OpenMcdf3/DirectoryEntry.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace OpenMcdf3; + +enum Color +{ + Red = 0, + Black = 1 +} + +internal sealed class DirectoryEntry +{ + internal const int Length = 128; + internal const int NameFieldLength = 64; + internal const uint MaxV3StreamLength = 0x80000000; + + internal static readonly DateTime ZeroFileTime = DateTime.FromFileTimeUtc(0); + + string name = string.Empty; + DateTime creationTime; + DateTime modifiedTime; + + public string Name + { + get => name; + set + { + if (value.Contains(@"\") || value.Contains(@"/") || value.Contains(@":") || value.Contains(@"!")) + throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':','!'"); + + if (Encoding.Unicode.GetByteCount(value) + 2 > NameFieldLength) + throw new ArgumentException($"{value} exceeds maximum encoded length of {NameFieldLength} bytes"); + + name = value; + } + } + + public StorageType Type { get; set; } = StorageType.Invalid; + + public Color Color { get; set; } + + public uint LeftSiblingID { get; set; } + + public uint RightSiblingID { get; set; } + + public uint ChildID { get; set; } + + public Guid CLSID { get; set; } + + public uint StateBits { get; set; } + + public DateTime CreationTime + { + get => creationTime; + set + { + if (Type is StorageType.Stream or StorageType.Root && value != ZeroFileTime) + throw new ArgumentException("Creation time must be zero for streams and root"); + + creationTime = value; + } + } + + public DateTime ModifiedTime + { + get => modifiedTime; + set + { + if (Type is StorageType.Stream && value != ZeroFileTime) + throw new ArgumentException("Modified time must be zero for streams"); + + modifiedTime = value; + } + } + + public uint StartSectorLocation { get; set; } + + public long StreamLength { get; set; } +} diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index 2f98116c..fe0c4c4d 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -1,7 +1,11 @@ -namespace OpenMcdf3; +using System.Text; + +namespace OpenMcdf3; internal class McdfBinaryReader : BinaryReader { + readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; + public McdfBinaryReader(Stream input) : base(input) { } @@ -52,4 +56,43 @@ public Header ReadHeader() return header; } + + public StorageType ReadStorageType() => (StorageType)ReadByte(); + + public Color ReadColor() => (Color)ReadByte(); + + public DirectoryEntry ReadDirectoryEntry(Version version) + { + if (version is not Version.V3 and not Version.V4) + throw new ArgumentException($"Unsupported version: {version}"); + + DirectoryEntry entry = new(); + Read(buffer, 0, DirectoryEntry.NameFieldLength); + int nameLength = Math.Max(0, ReadUInt16() - 2); + entry.Name = Encoding.Unicode.GetString(buffer, 0, nameLength); + entry.Type = ReadStorageType(); + entry.Color = ReadColor(); + entry.LeftSiblingID = ReadUInt32(); + entry.RightSiblingID = ReadUInt32(); + entry.ChildID = ReadUInt32(); + entry.CLSID = ReadGuid(); + entry.StateBits = ReadUInt32(); + entry.CreationTime = ReadFileTime(); + entry.ModifiedTime = ReadFileTime(); + entry.StartSectorLocation = ReadUInt32(); + + if (version == Version.V3) + { + entry.StreamLength = ReadUInt32(); + if (entry.StreamLength > DirectoryEntry.MaxV3StreamLength) + throw new FormatException($"Stream length {entry.StreamLength} exceeds maximum value {DirectoryEntry.MaxV3StreamLength}"); + ReadUInt32(); // Skip unused 4 bytes + } + else if (version == Version.V4) + { + entry.StreamLength = ReadInt64(); + } + + return entry; + } } diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index e9befdf0..9f38a07c 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -1,10 +1,11 @@ -using System.IO; -using System.Security.Claims; +using System.Text; namespace OpenMcdf3; internal class McdfBinaryWriter : BinaryWriter { + readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; + public McdfBinaryWriter(Stream input) : base(input) { } @@ -44,4 +45,22 @@ public void Write(Header header) Write(header.FirstDifatSectorID); Write(header.DifatSectorCount); } + + public void Write(DirectoryEntry entry) + { + int nameLength = Encoding.Unicode.GetBytes(entry.Name, 0, entry.Name.Length, buffer, 0); + Write(nameLength); + Write(buffer, 0, DirectoryEntry.NameFieldLength); + Write((byte)entry.Type); + Write((byte)entry.Color); + Write(entry.LeftSiblingID); + Write(entry.RightSiblingID); + Write(entry.ChildID); + Write(entry.CLSID); + Write(entry.StateBits); + Write(entry.CreationTime); + Write(entry.ModifiedTime); + Write(entry.StartSectorLocation); + Write(entry.StreamLength); + } } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 8bc4cfe4..a4aa9bcc 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -1,5 +1,15 @@ namespace OpenMcdf3; +public enum StorageType +{ + Invalid = 0, + Storage = 1, + Stream = 2, + Lockbytes = 3, + Property = 4, + Root = 5 +} + public class Storage { } From 95a0c773e5009acd35dbf77202278faf1666deab Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 10 Oct 2024 23:11:36 +1300 Subject: [PATCH 005/114] Enumerate directory entries --- OpenMcdf3.Tests/EntryInfoTests.cs | 13 ++++ OpenMcdf3/EntryInfo.cs | 6 ++ OpenMcdf3/McdfBinaryReader.cs | 2 + OpenMcdf3/RootStorage.cs | 103 +++++++++++++++++++++++++++--- OpenMcdf3/Sector.cs | 16 ++++- 5 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 OpenMcdf3.Tests/EntryInfoTests.cs create mode 100644 OpenMcdf3/EntryInfo.cs diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf3.Tests/EntryInfoTests.cs new file mode 100644 index 00000000..33e3c850 --- /dev/null +++ b/OpenMcdf3.Tests/EntryInfoTests.cs @@ -0,0 +1,13 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class EntryInfoTests +{ + [TestMethod] + [DataRow("_Test.ppt", 5)] + public void EnumerateEntryInfos(string fileName, int count) + { + using var rootStorage = RootStorage.Open(fileName, FileMode.Open); + Assert.AreEqual(count, rootStorage.EnumerateEntries().Count()); + } +} diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs new file mode 100644 index 00000000..40dd2066 --- /dev/null +++ b/OpenMcdf3/EntryInfo.cs @@ -0,0 +1,6 @@ +namespace OpenMcdf3; + +public class EntryInfo +{ + public string Name { get; internal set; } +} diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index fe0c4c4d..91ecf4f1 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -95,4 +95,6 @@ public DirectoryEntry ReadDirectoryEntry(Version version) return entry; } + + public void Seek(long offset) => BaseStream.Seek(offset, SeekOrigin.Begin); } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index c1c1b2b6..a3111efe 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf3; +using System.Diagnostics; + +namespace OpenMcdf3; public enum Version : ushort { @@ -6,18 +8,12 @@ public enum Version : ushort V4 = 4 } -public sealed class RootStorage : Storage +public sealed class RootStorage : Storage, IDisposable { readonly Header header; readonly McdfBinaryReader reader; readonly McdfBinaryWriter? writer; - - RootStorage(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) - { - this.header = header; - this.reader = reader; - this.writer = writer; - } + bool disposed; public static RootStorage Create(string fileName, Version version = Version.V3) { @@ -35,4 +31,93 @@ public static RootStorage Open(string fileName, FileMode mode) Header header = reader.ReadHeader(); return new RootStorage(header, reader); } + + RootStorage(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) + { + this.header = header; + this.reader = reader; + this.writer = writer; + } + + public void Dispose() + { + if (disposed) + return; + + writer?.Dispose(); + reader.Dispose(); + disposed = true; + } + + IEnumerable EnumerateDifatSectorChain() + { + uint nextId = header.FirstDifatSectorID; + while (nextId != (uint)SectorType.EndOfChain) + { + Sector s = new(nextId, header.SectorSize); + yield return s; + long nextIdOffset = s.EndOffset - sizeof(uint); + reader.Seek(nextIdOffset); + nextId = reader.ReadUInt32(); + } + } + + IEnumerable EnumerateFatSectors() + { + for (uint i = 0; i < header.FatSectorCount && i < Header.DifatLength; i++) + { + uint nextId = header.Difat[i]; + Sector s = new(nextId, header.SectorSize); + yield return s; + } + + foreach (Sector difatSector in EnumerateDifatSectorChain()) + { + reader.Seek(difatSector.StartOffset); + int difatElementCount = header.SectorSize / sizeof(uint) - 1; + for (int i = 0; i < difatElementCount; i++) + { + uint nextId = reader.ReadUInt32(); + Sector s = new(nextId, header.SectorSize); + yield return s; + } + } + } + + uint GetNextFatSectorId(uint id) + { + int elementLength = header.SectorSize / sizeof(uint); + int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); + Sector sector = EnumerateFatSectors().ElementAt(sectorId); + long position = sector.StartOffset + sectorOffset * sizeof(uint); + reader.Seek(position); + uint nextId = reader.ReadUInt32(); + return nextId; + } + + IEnumerable EnumerateFatSectorChain(uint id) + { + while (id != (uint)SectorType.EndOfChain) + { + Sector sector = new(id, header.SectorSize); + yield return sector; + id = GetNextFatSectorId(id); + } + } + + public IEnumerable EnumerateEntries() + { + foreach (Sector sector in EnumerateFatSectorChain(header.FirstDirectorySectorID)) + { + reader.Seek(sector.StartOffset); + + int entryCount = header.SectorSize / DirectoryEntry.Length; + for (int i = 0; i < entryCount; i++) + { + DirectoryEntry entry = reader.ReadDirectoryEntry((Version)header.MajorVersion); + if (entry.Type is not StorageType.Invalid) + yield return new EntryInfo { Name = entry.Name }; + } + } + } } diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 18b27bb2..75700be3 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -1,6 +1,20 @@ namespace OpenMcdf3; -internal class Sector +internal struct Sector { const int MiniSectorSize = 64; + + public Sector(uint index, long length) + { + Index = index; + Length = length; + } + + public uint Index { get; } + + public long Length { get; } + + public readonly long StartOffset => (Index + 1) * Length; + + public readonly long EndOffset => (Index + 2) * Length; } From 264fe6c3fcfde511b50a36d377b18f05c92a335c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 11 Oct 2024 11:11:39 +1300 Subject: [PATCH 006/114] Open and read streams --- OpenMcdf3.Tests/CfbStreamTests.cs | 19 +++++++ OpenMcdf3.Tests/EntryInfoTests.cs | 1 + OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 3 + OpenMcdf3.Tests/test.cfb | Bin 0 -> 1058304 bytes OpenMcdf3/CfbStream.cs | 73 +++++++++++++++++++++++++ OpenMcdf3/RootStorage.cs | 32 ++++++++--- 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 OpenMcdf3.Tests/CfbStreamTests.cs create mode 100644 OpenMcdf3.Tests/test.cfb create mode 100644 OpenMcdf3/CfbStream.cs diff --git a/OpenMcdf3.Tests/CfbStreamTests.cs b/OpenMcdf3.Tests/CfbStreamTests.cs new file mode 100644 index 00000000..464a8a4a --- /dev/null +++ b/OpenMcdf3.Tests/CfbStreamTests.cs @@ -0,0 +1,19 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class CfbStreamTests +{ + [TestMethod] + [DataRow("_Test.ppt", "Current User", 62)] + [DataRow("test.cfb", "MyStream0", 1048576)] + public void CfbStreamTest(string fileName, string streamName, long length) + { + using var rootStorage = RootStorage.Open(fileName, FileMode.Open); + using var stream = rootStorage.OpenStream(streamName); + Assert.AreEqual(length, stream.Length); + + using MemoryStream memoryStream = new(); + stream.CopyTo(memoryStream); + Assert.AreEqual(length, memoryStream.Length); + } +} diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf3.Tests/EntryInfoTests.cs index 33e3c850..0db90a0f 100644 --- a/OpenMcdf3.Tests/EntryInfoTests.cs +++ b/OpenMcdf3.Tests/EntryInfoTests.cs @@ -5,6 +5,7 @@ public sealed class EntryInfoTests { [TestMethod] [DataRow("_Test.ppt", 5)] + [DataRow("test.cfb", 2)] public void EnumerateEntryInfos(string fileName, int count) { using var rootStorage = RootStorage.Open(fileName, FileMode.Open); diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 664cdf8c..009e0b55 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -26,6 +26,9 @@ + + PreserveNewest + PreserveNewest diff --git a/OpenMcdf3.Tests/test.cfb b/OpenMcdf3.Tests/test.cfb new file mode 100644 index 0000000000000000000000000000000000000000..861c57cbcc2b12a208cdab4262854f362298ca98 GIT binary patch literal 1058304 zcmeI&RSa|2p`g*fVPN8SCu<|Hn4>fPn}6{>P952K%@BN9_8DL;SBhMi|ie zAAkJo)_?2=H1?+&s0OY7}8m@+~5voJl2oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ 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 R_vZhvAOAml true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => length; + + public override long Position + { + get => position; + set => position = value; + } + + public override void Flush() + { + //rootStorage.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int sectorSkipCount = (int)Math.DivRem(position, sectorLength, out long sectorOffset); + int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + int realCount = Math.Min(count, maxCount); + int readCount = 0; + int remaining = realCount; + foreach (Sector sector in rootStorage.EnumerateFatSectorChain(directoryEntry.StartSectorLocation).Skip(sectorSkipCount)) + { + long readLength = Math.Min(remaining, sector.Length - sectorOffset); + rootStorage.Reader.Seek(sector.StartOffset + sectorOffset); + int read = rootStorage.Reader.Read(buffer, offset, (int)readLength); + if (read == 0) + return 0; + position += read; + readCount += read; + if (readCount >= realCount) + return readCount; + } + + return readCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + position = offset; + return position; + } + + public override void SetLength(long value) + { + length = value; + } + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index a3111efe..fc77dd5c 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -15,6 +15,8 @@ public sealed class RootStorage : Storage, IDisposable readonly McdfBinaryWriter? writer; bool disposed; + internal McdfBinaryReader Reader => reader; + public static RootStorage Create(string fileName, Version version = Version.V3) { FileStream stream = File.Create(fileName); @@ -27,9 +29,15 @@ public static RootStorage Create(string fileName, Version version = Version.V3) public static RootStorage Open(string fileName, FileMode mode) { FileStream stream = File.Open(fileName, mode); + return Open(stream); + } + + public static RootStorage Open(Stream stream) + { McdfBinaryReader reader = new(stream); + McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); - return new RootStorage(header, reader); + return new RootStorage(header, reader, writer); } RootStorage(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) @@ -95,17 +103,18 @@ uint GetNextFatSectorId(uint id) return nextId; } - IEnumerable EnumerateFatSectorChain(uint id) + internal IEnumerable EnumerateFatSectorChain(uint startId) { - while (id != (uint)SectorType.EndOfChain) + uint nextId = startId; + while (nextId is not (uint)SectorType.EndOfChain and not (uint)SectorType.Free) { - Sector sector = new(id, header.SectorSize); + Sector sector = new(nextId, header.SectorSize); yield return sector; - id = GetNextFatSectorId(id); + nextId = GetNextFatSectorId(nextId); } } - public IEnumerable EnumerateEntries() + IEnumerable EnumerateDirectoryEntries() { foreach (Sector sector in EnumerateFatSectorChain(header.FirstDirectorySectorID)) { @@ -116,8 +125,17 @@ public IEnumerable EnumerateEntries() { DirectoryEntry entry = reader.ReadDirectoryEntry((Version)header.MajorVersion); if (entry.Type is not StorageType.Invalid) - yield return new EntryInfo { Name = entry.Name }; + yield return entry; } } } + + public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries().Select(e => new EntryInfo { Name = e.Name }); + + public CfbStream OpenStream(string name) + { + DirectoryEntry? entry = EnumerateDirectoryEntries() + .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); + return new CfbStream(this, header.SectorSize, entry); + } } From e9baa90fd40f162d5311f30a81d37fac084d7075 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 11 Oct 2024 11:55:33 +1300 Subject: [PATCH 007/114] Add benchmark --- OpenMcdf3.Benchmarks/InMemory.cs | 97 +++++++++++++++++++ .../OpenMcdf3.Benchmarks.csproj | 19 ++++ OpenMcdf3.Benchmarks/Program.cs | 11 +++ OpenMcdf3.sln | 14 ++- OpenMcdf3/McdfBinaryReader.cs | 3 +- OpenMcdf3/McdfBinaryWriter.cs | 3 +- OpenMcdf3/RootStorage.cs | 1 + 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 OpenMcdf3.Benchmarks/InMemory.cs create mode 100644 OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj create mode 100644 OpenMcdf3.Benchmarks/Program.cs diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs new file mode 100644 index 00000000..c33a02f1 --- /dev/null +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -0,0 +1,97 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using OpenMcdf; + +namespace OpenMcdf3.Benchmark; + +[SimpleJob] +[CsvExporter] +[HtmlExporter] +[MarkdownExporter] +//[DryCoreJob] // I always forget this attribute, so please leave it commented out +[MemoryDiagnoser] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class InMemory : IDisposable +{ + private const int Kb = 1024; + private const int Mb = Kb * Kb; + private const string storageName = "MyStorage"; + private const string streamName = "MyStream"; + + private byte[] _readBuffer; + + private MemoryStream _stream; + + [Params(Kb / 2, Kb, 4 * Kb, 128 * Kb, 256 * Kb, 512 * Kb, Kb * Kb)] + public int BufferSize { get; set; } + + [Params(Mb /*, 8 * Mb, 64 * Mb, 128 * Mb*/)] + public int TotalStreamSize { get; set; } + + public void Dispose() + { + _stream?.Dispose(); + } + + [GlobalSetup] + public void GlobalSetup() + { + _stream = new MemoryStream(); + //_stream = File.Create("D:\\test.cfb"); + _readBuffer = new byte[BufferSize]; + CreateFile(1); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + _stream.Dispose(); + _stream = null; + _readBuffer = null; + } + + [Benchmark] + public void Test() + { + // + _stream.Seek(0L, SeekOrigin.Begin); + // + using var compoundFile = RootStorage.Open(_stream); + using CfbStream cfStream = compoundFile.OpenStream(streamName + 0); + long streamSize = cfStream.Length; + long position = 0L; + while (true) + { + if (position >= streamSize) + break; + int read = cfStream.Read(_readBuffer, 0, _readBuffer.Length); + position += read; + if (read <= 0) break; + } + + //compoundFile.Close(); + } + + private void CreateFile(int streamCount) + { + var iterationCount = TotalStreamSize / BufferSize; + + var buffer = new byte[BufferSize]; + Array.Fill(buffer, byte.MaxValue); + const CFSConfiguration flags = CFSConfiguration.Default | CFSConfiguration.LeaveOpen; + using (var compoundFile = new CompoundFile(CFSVersion.Ver_3, flags)) + { + //var st = compoundFile.RootStorage.AddStorage(storageName); + var st = compoundFile.RootStorage; + for (var streamId = 0; streamId < streamCount; ++streamId) + { + var sm = st.AddStream(streamName + streamId); + + for (var iteration = 0; iteration < iterationCount; ++iteration) sm.Append(buffer); + } + + compoundFile.Save(_stream); + compoundFile.Close(); + } + } +} diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj new file mode 100644 index 00000000..94e38e90 --- /dev/null +++ b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/OpenMcdf3.Benchmarks/Program.cs b/OpenMcdf3.Benchmarks/Program.cs new file mode 100644 index 00000000..f00ccb3e --- /dev/null +++ b/OpenMcdf3.Benchmarks/Program.cs @@ -0,0 +1,11 @@ +using BenchmarkDotNet.Running; + +namespace OpenMcdf3.Benchmarks; + +internal class Program +{ + private static void Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 5999bf3c..4f19d749 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -3,16 +3,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3", "D:\OpenMcdf3\OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3", "D:\OpenMcdf3\OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3.Tests", "D:\OpenMcdf3\OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Tests", "D:\OpenMcdf3\OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34030FA7-0A06-43D1-85DD-ADD39D502C3C}" ProjectSection(SolutionItems) = preProject - D:\OpenMcdf3\.editorconfig = D:\OpenMcdf3\.editorconfig - D:\OpenMcdf3\.globalconfig = D:\OpenMcdf3\.globalconfig + .editorconfig = .editorconfig + .globalconfig = .globalconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3.Benchmarks", "OpenMcdf3.Benchmarks\OpenMcdf3.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Debug|Any CPU.Build.0 = Debug|Any CPU {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.ActiveCfg = Release|Any CPU {96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}.Release|Any CPU.Build.0 = Release|Any CPU + {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44C718AD-F7FE-4733-80A8-636E5E7E63F3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index 91ecf4f1..441445bf 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -6,7 +6,8 @@ internal class McdfBinaryReader : BinaryReader { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; - public McdfBinaryReader(Stream input) : base(input) + public McdfBinaryReader(Stream input) + : base(input, Encoding.Unicode, true) { } diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index 9f38a07c..067036f1 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -6,7 +6,8 @@ internal class McdfBinaryWriter : BinaryWriter { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; - public McdfBinaryWriter(Stream input) : base(input) + public McdfBinaryWriter(Stream input) + : base(input, Encoding.Unicode, true) { } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index fc77dd5c..345d6476 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -54,6 +54,7 @@ public void Dispose() writer?.Dispose(); reader.Dispose(); + reader.BaseStream.Dispose(); disposed = true; } From aa8fd9e2b4d68e73d4a223804a2fc91c0615a414 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 11 Oct 2024 12:51:08 +1300 Subject: [PATCH 008/114] Cache sectors --- OpenMcdf3/RootStorage.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 345d6476..06db0198 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -54,7 +54,7 @@ public void Dispose() writer?.Dispose(); reader.Dispose(); - reader.BaseStream.Dispose(); + //reader.BaseStream.Dispose(); disposed = true; } @@ -93,11 +93,14 @@ IEnumerable EnumerateFatSectors() } } + List fatSectors; + uint GetNextFatSectorId(uint id) { int elementLength = header.SectorSize / sizeof(uint); int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); - Sector sector = EnumerateFatSectors().ElementAt(sectorId); + fatSectors ??= EnumerateFatSectors().ToList(); + Sector sector = fatSectors[sectorId]; long position = sector.StartOffset + sectorOffset * sizeof(uint); reader.Seek(position); uint nextId = reader.ReadUInt32(); From c58349a62b6e84a43f9f8e8714a6a530f634e0f5 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sun, 13 Oct 2024 14:47:04 +1300 Subject: [PATCH 009/114] Add IOContext --- OpenMcdf3/CfbStream.cs | 16 ++++--- OpenMcdf3/ChainIterator.cs | 57 +++++++++++++++++++++++ OpenMcdf3/IOContext.cs | 68 ++++++++++++++++++++++++++++ OpenMcdf3/RootStorage.cs | 93 ++++++-------------------------------- 4 files changed, 148 insertions(+), 86 deletions(-) create mode 100644 OpenMcdf3/ChainIterator.cs create mode 100644 OpenMcdf3/IOContext.cs diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index ff19beba..03437e9b 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -2,18 +2,20 @@ public class CfbStream : Stream { - readonly RootStorage rootStorage; + readonly IOContext ioContext; readonly long sectorLength; - private readonly DirectoryEntry directoryEntry; + readonly DirectoryEntry directoryEntry; + readonly List sectorChain; long length; long position; - internal CfbStream(RootStorage rootStorage, long sectorLength, DirectoryEntry directoryEntry) + internal CfbStream(IOContext ioContext, long sectorLength, DirectoryEntry directoryEntry) { - this.rootStorage = rootStorage; + this.ioContext = ioContext; this.sectorLength = sectorLength; this.directoryEntry = directoryEntry; length = directoryEntry.StreamLength; + sectorChain = ioContext.EnumerateFatSectorChain(directoryEntry.StartSectorLocation).ToList(); } public override bool CanRead => true; @@ -42,11 +44,11 @@ public override int Read(byte[] buffer, int offset, int count) int realCount = Math.Min(count, maxCount); int readCount = 0; int remaining = realCount; - foreach (Sector sector in rootStorage.EnumerateFatSectorChain(directoryEntry.StartSectorLocation).Skip(sectorSkipCount)) + foreach (Sector sector in sectorChain.Skip(sectorSkipCount)) { long readLength = Math.Min(remaining, sector.Length - sectorOffset); - rootStorage.Reader.Seek(sector.StartOffset + sectorOffset); - int read = rootStorage.Reader.Read(buffer, offset, (int)readLength); + ioContext.Reader.Seek(sector.StartOffset + sectorOffset); + int read = ioContext.Reader.Read(buffer, offset, (int)readLength); if (read == 0) return 0; position += read; diff --git a/OpenMcdf3/ChainIterator.cs b/OpenMcdf3/ChainIterator.cs new file mode 100644 index 00000000..9ef5ec4d --- /dev/null +++ b/OpenMcdf3/ChainIterator.cs @@ -0,0 +1,57 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal class ChainIterator : IEnumerator +{ + readonly IOContext ioContext; + private uint nextId; + + public ChainIterator(IOContext ioContext) + { + this.ioContext = ioContext; + this.nextId = ioContext.Header.FirstDifatSectorID; + Current = default; + } + + public Sector Current { get; private set; } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (nextId == (uint)SectorType.EndOfChain) + return false; + + Current = new Sector(nextId, ioContext.Header.SectorSize); + long nextIdOffset = Current.EndOffset - sizeof(uint); + ioContext.Reader.Seek(nextIdOffset); + nextId = ioContext.Reader.ReadUInt32(); + return true; + } + + public void Reset() + { + nextId = ioContext.Header.FirstDifatSectorID; + Current = default; + } + + public void Dispose() + { + // No resources to dispose + } +} + +internal class ChainEnumerable : IEnumerable +{ + private readonly IOContext ioContext; + + public ChainEnumerable(IOContext ioContext) + { + this.ioContext = ioContext; + } + + public IEnumerator GetEnumerator() => (IEnumerator)(new ChainIterator(ioContext)); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs new file mode 100644 index 00000000..6f1b0204 --- /dev/null +++ b/OpenMcdf3/IOContext.cs @@ -0,0 +1,68 @@ +namespace OpenMcdf3; + +internal sealed class IOContext : IDisposable +{ + public Header Header { get; } + public McdfBinaryReader Reader { get; } + public McdfBinaryWriter? Writer { get; } + List fatSectors; + + public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) + { + Header = header; + Reader = reader; + Writer = writer; + } + + public void Dispose() + { + Reader.Dispose(); + Writer?.Dispose(); + } + + IEnumerable EnumerateFatSectors() + { + for (uint i = 0; i < Header.FatSectorCount && i < Header.DifatLength; i++) + { + uint nextId = Header.Difat[i]; + Sector s = new(nextId, Header.SectorSize); + yield return s; + } + + ChainEnumerable iterator = new(this); + foreach (Sector difatSector in iterator) + { + Reader.Seek(difatSector.StartOffset); + int difatElementCount = Header.SectorSize / sizeof(uint) - 1; + for (int i = 0; i < difatElementCount; i++) + { + uint nextId = Reader.ReadUInt32(); + Sector s = new(nextId, Header.SectorSize); + yield return s; + } + } + } + + uint GetNextFatSectorId(uint id) + { + int elementLength = Header.SectorSize / sizeof(uint); + int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); + fatSectors ??= EnumerateFatSectors().ToList(); + Sector sector = fatSectors[sectorId]; + long position = sector.StartOffset + sectorOffset * sizeof(uint); + Reader.Seek(position); + uint nextId = Reader.ReadUInt32(); + return nextId; + } + + internal IEnumerable EnumerateFatSectorChain(uint startId) + { + uint nextId = startId; + while (nextId is not (uint)SectorType.EndOfChain and not (uint)SectorType.Free) + { + Sector sector = new(nextId, Header.SectorSize); + yield return sector; + nextId = GetNextFatSectorId(nextId); + } + } +} diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 06db0198..5e2dbefe 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -10,20 +10,17 @@ public enum Version : ushort public sealed class RootStorage : Storage, IDisposable { - readonly Header header; - readonly McdfBinaryReader reader; - readonly McdfBinaryWriter? writer; + readonly IOContext ioContext; bool disposed; - internal McdfBinaryReader Reader => reader; - public static RootStorage Create(string fileName, Version version = Version.V3) { FileStream stream = File.Create(fileName); Header header = new(version); McdfBinaryReader reader = new(stream); McdfBinaryWriter writer = new(stream); - return new RootStorage(header, reader, writer); + IOContext ioContext = new(header, reader, writer); + return new RootStorage(ioContext); } public static RootStorage Open(string fileName, FileMode mode) @@ -37,14 +34,13 @@ public static RootStorage Open(Stream stream) McdfBinaryReader reader = new(stream); McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); - return new RootStorage(header, reader, writer); + IOContext ioContext = new(header, reader, writer); + return new RootStorage(ioContext); } - RootStorage(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) + RootStorage(IOContext ioContext) { - this.header = header; - this.reader = reader; - this.writer = writer; + this.ioContext = ioContext; } public void Dispose() @@ -52,82 +48,21 @@ public void Dispose() if (disposed) return; - writer?.Dispose(); - reader.Dispose(); - //reader.BaseStream.Dispose(); + ioContext.Writer?.Dispose(); + ioContext.Reader.Dispose(); disposed = true; } - IEnumerable EnumerateDifatSectorChain() - { - uint nextId = header.FirstDifatSectorID; - while (nextId != (uint)SectorType.EndOfChain) - { - Sector s = new(nextId, header.SectorSize); - yield return s; - long nextIdOffset = s.EndOffset - sizeof(uint); - reader.Seek(nextIdOffset); - nextId = reader.ReadUInt32(); - } - } - - IEnumerable EnumerateFatSectors() - { - for (uint i = 0; i < header.FatSectorCount && i < Header.DifatLength; i++) - { - uint nextId = header.Difat[i]; - Sector s = new(nextId, header.SectorSize); - yield return s; - } - - foreach (Sector difatSector in EnumerateDifatSectorChain()) - { - reader.Seek(difatSector.StartOffset); - int difatElementCount = header.SectorSize / sizeof(uint) - 1; - for (int i = 0; i < difatElementCount; i++) - { - uint nextId = reader.ReadUInt32(); - Sector s = new(nextId, header.SectorSize); - yield return s; - } - } - } - - List fatSectors; - - uint GetNextFatSectorId(uint id) - { - int elementLength = header.SectorSize / sizeof(uint); - int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); - fatSectors ??= EnumerateFatSectors().ToList(); - Sector sector = fatSectors[sectorId]; - long position = sector.StartOffset + sectorOffset * sizeof(uint); - reader.Seek(position); - uint nextId = reader.ReadUInt32(); - return nextId; - } - - internal IEnumerable EnumerateFatSectorChain(uint startId) - { - uint nextId = startId; - while (nextId is not (uint)SectorType.EndOfChain and not (uint)SectorType.Free) - { - Sector sector = new(nextId, header.SectorSize); - yield return sector; - nextId = GetNextFatSectorId(nextId); - } - } - IEnumerable EnumerateDirectoryEntries() { - foreach (Sector sector in EnumerateFatSectorChain(header.FirstDirectorySectorID)) + foreach (Sector sector in ioContext.EnumerateFatSectorChain(ioContext.Header.FirstDirectorySectorID)) { - reader.Seek(sector.StartOffset); + ioContext.Reader.Seek(sector.StartOffset); - int entryCount = header.SectorSize / DirectoryEntry.Length; + int entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; for (int i = 0; i < entryCount; i++) { - DirectoryEntry entry = reader.ReadDirectoryEntry((Version)header.MajorVersion); + DirectoryEntry entry = ioContext.Reader.ReadDirectoryEntry((Version)ioContext.Header.MajorVersion); if (entry.Type is not StorageType.Invalid) yield return entry; } @@ -140,6 +75,6 @@ public CfbStream OpenStream(string name) { DirectoryEntry? entry = EnumerateDirectoryEntries() .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); - return new CfbStream(this, header.SectorSize, entry); + return new CfbStream(ioContext, ioContext.Header.SectorSize, entry); } } From 2e69028c41dcd11e81177132e558b486520cb735 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sun, 13 Oct 2024 15:20:32 +1300 Subject: [PATCH 010/114] Add enumerators --- OpenMcdf3.Tests/CfbStreamTests.cs | 4 +- OpenMcdf3.Tests/EntryInfoTests.cs | 5 +- OpenMcdf3/CfbStream.cs | 20 ++++-- OpenMcdf3/ChainIterator.cs | 57 ---------------- OpenMcdf3/EntryInfo.cs | 2 + OpenMcdf3/FatSectorChainEnumerator.cs | 61 +++++++++++++++++ OpenMcdf3/FatSectorEnumerator.cs | 94 +++++++++++++++++++++++++++ OpenMcdf3/Header.cs | 8 +-- OpenMcdf3/IOContext.cs | 47 -------------- OpenMcdf3/RootStorage.cs | 11 +++- OpenMcdf3/Sector.cs | 16 ++--- OpenMcdf3/SectorType.cs | 12 ++-- 12 files changed, 200 insertions(+), 137 deletions(-) delete mode 100644 OpenMcdf3/ChainIterator.cs create mode 100644 OpenMcdf3/FatSectorChainEnumerator.cs create mode 100644 OpenMcdf3/FatSectorEnumerator.cs diff --git a/OpenMcdf3.Tests/CfbStreamTests.cs b/OpenMcdf3.Tests/CfbStreamTests.cs index 464a8a4a..037f99c9 100644 --- a/OpenMcdf3.Tests/CfbStreamTests.cs +++ b/OpenMcdf3.Tests/CfbStreamTests.cs @@ -8,8 +8,8 @@ public sealed class CfbStreamTests [DataRow("test.cfb", "MyStream0", 1048576)] public void CfbStreamTest(string fileName, string streamName, long length) { - using var rootStorage = RootStorage.Open(fileName, FileMode.Open); - using var stream = rootStorage.OpenStream(streamName); + using var rootStorage = RootStorage.OpenRead(fileName); + using CfbStream stream = rootStorage.OpenStream(streamName); Assert.AreEqual(length, stream.Length); using MemoryStream memoryStream = new(); diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf3.Tests/EntryInfoTests.cs index 0db90a0f..3af05877 100644 --- a/OpenMcdf3.Tests/EntryInfoTests.cs +++ b/OpenMcdf3.Tests/EntryInfoTests.cs @@ -8,7 +8,8 @@ public sealed class EntryInfoTests [DataRow("test.cfb", 2)] public void EnumerateEntryInfos(string fileName, int count) { - using var rootStorage = RootStorage.Open(fileName, FileMode.Open); - Assert.AreEqual(count, rootStorage.EnumerateEntries().Count()); + using var rootStorage = RootStorage.OpenRead(fileName); + IEnumerable entries = rootStorage.EnumerateEntries(); + Assert.AreEqual(count, entries.Count()); } } diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index 03437e9b..d060d714 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -4,8 +4,7 @@ public class CfbStream : Stream { readonly IOContext ioContext; readonly long sectorLength; - readonly DirectoryEntry directoryEntry; - readonly List sectorChain; + readonly FatSectorChainEnumerator chain; long length; long position; @@ -13,11 +12,13 @@ internal CfbStream(IOContext ioContext, long sectorLength, DirectoryEntry direct { this.ioContext = ioContext; this.sectorLength = sectorLength; - this.directoryEntry = directoryEntry; + DirectoryEntry = directoryEntry; length = directoryEntry.StreamLength; - sectorChain = ioContext.EnumerateFatSectorChain(directoryEntry.StartSectorLocation).ToList(); + chain = new(ioContext, directoryEntry.StartSectorLocation); } + internal DirectoryEntry DirectoryEntry { get; private set; } + public override bool CanRead => true; public override bool CanSeek => true; @@ -39,13 +40,20 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { - int sectorSkipCount = (int)Math.DivRem(position, sectorLength, out long sectorOffset); + int sectorIndex = (int)Math.DivRem(position, sectorLength, out long sectorOffset); + while (sectorIndex > 0 && (chain.Index == SectorType.EndOfChain || chain.Index < sectorIndex)) + { + if (!chain.MoveNext()) + return 0; + } + int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); int realCount = Math.Min(count, maxCount); int readCount = 0; int remaining = realCount; - foreach (Sector sector in sectorChain.Skip(sectorSkipCount)) + while (chain.MoveNext()) { + Sector sector = chain.Current; long readLength = Math.Min(remaining, sector.Length - sectorOffset); ioContext.Reader.Seek(sector.StartOffset + sectorOffset); int read = ioContext.Reader.Read(buffer, offset, (int)readLength); diff --git a/OpenMcdf3/ChainIterator.cs b/OpenMcdf3/ChainIterator.cs deleted file mode 100644 index 9ef5ec4d..00000000 --- a/OpenMcdf3/ChainIterator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections; - -namespace OpenMcdf3; - -internal class ChainIterator : IEnumerator -{ - readonly IOContext ioContext; - private uint nextId; - - public ChainIterator(IOContext ioContext) - { - this.ioContext = ioContext; - this.nextId = ioContext.Header.FirstDifatSectorID; - Current = default; - } - - public Sector Current { get; private set; } - - object IEnumerator.Current => Current; - - public bool MoveNext() - { - if (nextId == (uint)SectorType.EndOfChain) - return false; - - Current = new Sector(nextId, ioContext.Header.SectorSize); - long nextIdOffset = Current.EndOffset - sizeof(uint); - ioContext.Reader.Seek(nextIdOffset); - nextId = ioContext.Reader.ReadUInt32(); - return true; - } - - public void Reset() - { - nextId = ioContext.Header.FirstDifatSectorID; - Current = default; - } - - public void Dispose() - { - // No resources to dispose - } -} - -internal class ChainEnumerable : IEnumerable -{ - private readonly IOContext ioContext; - - public ChainEnumerable(IOContext ioContext) - { - this.ioContext = ioContext; - } - - public IEnumerator GetEnumerator() => (IEnumerator)(new ChainIterator(ioContext)); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs index 40dd2066..8904164a 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf3/EntryInfo.cs @@ -3,4 +3,6 @@ public class EntryInfo { public string Name { get; internal set; } + + public override string ToString() => Name; } diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs new file mode 100644 index 00000000..a7ea20ba --- /dev/null +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -0,0 +1,61 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal sealed class FatSectorChainEnumerator : IEnumerator +{ + private readonly FatSectorEnumerator fatEnumerator; + private readonly IOContext ioContext; + private readonly uint startId; + private uint nextId; + private Sector current; + + public FatSectorChainEnumerator(IOContext ioContext, uint startId) + { + fatEnumerator = new(ioContext); + this.ioContext = ioContext; + Index = SectorType.EndOfChain; + this.startId = startId; + this.nextId = SectorType.Free; + this.current = new Sector(0, 0); // Initialize with a default value + } + + public uint Index { get; private set; } + + public Sector Current => current; + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (nextId is SectorType.Free) + { + Index = 0; + nextId = startId; + } + + if (nextId is SectorType.EndOfChain) + { + Index = SectorType.EndOfChain; + return false; + } + + Index++; + current = new Sector(nextId, ioContext.Header.SectorSize); + nextId = fatEnumerator.GetNextFatSectorId(nextId); + return true; + } + + public void Reset() + { + Index = SectorType.EndOfChain; + nextId = SectorType.Free; + current = new Sector(0, 0); // Reset to default value + fatEnumerator.Reset(); + } + + public void Dispose() + { + // No resources to dispose + } +} diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs new file mode 100644 index 00000000..c34bb474 --- /dev/null +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -0,0 +1,94 @@ +using System.Collections; +using System.Diagnostics.SymbolStore; + +namespace OpenMcdf3; + +internal sealed class FatSectorEnumerator : IEnumerator +{ + private readonly IOContext ioContext; + private uint index = SectorType.EndOfChain; + uint nextDifatSectorId = SectorType.EndOfChain; + uint difatSectorElementIndex = SectorType.EndOfChain; + + public FatSectorEnumerator(IOContext ioContext) + { + this.ioContext = ioContext; + this.index = SectorType.EndOfChain; + this.nextDifatSectorId = ioContext.Header.FirstDifatSectorID; + Current = default; + } + + public Sector Current { get; private set; } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (index == SectorType.EndOfChain) + { + index = 0; + } + + if (index < ioContext.Header.FatSectorCount && index < Header.DifatLength) + { + uint nextId = ioContext.Header.Difat[index]; + Current = new Sector(nextId, ioContext.Header.SectorSize); + index++; + return true; + } + + if (nextDifatSectorId == SectorType.EndOfChain) + return false; + + int difatElementCount = ioContext.Header.SectorSize / sizeof(uint) - 1; + Sector difatSector = new(nextDifatSectorId, ioContext.Header.SectorSize); + if (difatSectorElementIndex == difatElementCount) + { + long nextIdOffset = difatSector.EndOffset - sizeof(uint); + ioContext.Reader.Seek(nextIdOffset); + nextDifatSectorId = ioContext.Reader.ReadUInt32(); + difatSectorElementIndex = 0; + } + + if (difatSectorElementIndex < difatElementCount) + { + long position = difatSector.StartOffset + difatSectorElementIndex * sizeof(uint); + ioContext.Reader.Seek(position); + uint nextId = ioContext.Reader.ReadUInt32(); + Current = new Sector(nextId, ioContext.Header.SectorSize); + difatSectorElementIndex++; + index++; + return true; + } + + return false; + } + + public void Reset() + { + index = SectorType.EndOfChain; + difatSectorElementIndex = SectorType.EndOfChain; + Current = new Sector(0, 0); // Reset to default value + } + + public uint GetNextFatSectorId(uint id) + { + int elementLength = ioContext.Header.SectorSize / sizeof(uint); + int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); + while (index == SectorType.EndOfChain || index - 1 < sectorId) + { + if (!MoveNext()) + return SectorType.EndOfChain; + } + + Sector sector = Current; + long position = sector.StartOffset + sectorOffset * sizeof(uint); + ioContext.Reader.Seek(position); + uint nextId = ioContext.Reader.ReadUInt32(); + return nextId; + } + + public void Dispose() + { + } +} diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 0ab79724..95e19d94 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -47,18 +47,18 @@ public ushort SectorShift public uint FatSectorCount { get; set; } - public uint FirstDirectorySectorID { get; set; } = (uint)SectorType.EndOfChain; + public uint FirstDirectorySectorID { get; set; } = SectorType.EndOfChain; public uint TransactionSignature { get; set; } /// /// This integer field contains the starting sector number for the mini FAT /// - public uint FirstMiniFatSectorID { get; set; } = (uint)SectorType.EndOfChain; + public uint FirstMiniFatSectorID { get; set; } = SectorType.EndOfChain; public uint MiniFatSectorCount { get; set; } - public uint FirstDifatSectorID { get; set; } = (uint)SectorType.EndOfChain; + public uint FirstDifatSectorID { get; set; } = SectorType.EndOfChain; public uint DifatSectorCount { get; set; } @@ -71,7 +71,7 @@ public Header(Version version = Version.V3) MajorVersion = (ushort)version; for (int i = 0; i < Difat.Length; i++) { - Difat[i] = (uint)SectorType.Free; + Difat[i] = SectorType.Free; } } } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 6f1b0204..abe22fbe 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -5,7 +5,6 @@ internal sealed class IOContext : IDisposable public Header Header { get; } public McdfBinaryReader Reader { get; } public McdfBinaryWriter? Writer { get; } - List fatSectors; public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) { @@ -19,50 +18,4 @@ public void Dispose() Reader.Dispose(); Writer?.Dispose(); } - - IEnumerable EnumerateFatSectors() - { - for (uint i = 0; i < Header.FatSectorCount && i < Header.DifatLength; i++) - { - uint nextId = Header.Difat[i]; - Sector s = new(nextId, Header.SectorSize); - yield return s; - } - - ChainEnumerable iterator = new(this); - foreach (Sector difatSector in iterator) - { - Reader.Seek(difatSector.StartOffset); - int difatElementCount = Header.SectorSize / sizeof(uint) - 1; - for (int i = 0; i < difatElementCount; i++) - { - uint nextId = Reader.ReadUInt32(); - Sector s = new(nextId, Header.SectorSize); - yield return s; - } - } - } - - uint GetNextFatSectorId(uint id) - { - int elementLength = Header.SectorSize / sizeof(uint); - int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); - fatSectors ??= EnumerateFatSectors().ToList(); - Sector sector = fatSectors[sectorId]; - long position = sector.StartOffset + sectorOffset * sizeof(uint); - Reader.Seek(position); - uint nextId = Reader.ReadUInt32(); - return nextId; - } - - internal IEnumerable EnumerateFatSectorChain(uint startId) - { - uint nextId = startId; - while (nextId is not (uint)SectorType.EndOfChain and not (uint)SectorType.Free) - { - Sector sector = new(nextId, Header.SectorSize); - yield return sector; - nextId = GetNextFatSectorId(nextId); - } - } } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 5e2dbefe..6792f402 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -29,6 +29,12 @@ public static RootStorage Open(string fileName, FileMode mode) return Open(stream); } + public static RootStorage OpenRead(string fileName) + { + FileStream stream = File.OpenRead(fileName); + return Open(stream); + } + public static RootStorage Open(Stream stream) { McdfBinaryReader reader = new(stream); @@ -55,9 +61,10 @@ public void Dispose() IEnumerable EnumerateDirectoryEntries() { - foreach (Sector sector in ioContext.EnumerateFatSectorChain(ioContext.Header.FirstDirectorySectorID)) + using FatSectorChainEnumerator chainEnumerator = new(ioContext, ioContext.Header.FirstDirectorySectorID); + while (chainEnumerator.MoveNext()) { - ioContext.Reader.Seek(sector.StartOffset); + ioContext.Reader.Seek(chainEnumerator.Current.StartOffset); int entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; for (int i = 0; i < entryCount; i++) diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 75700be3..8974d320 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -1,20 +1,14 @@ namespace OpenMcdf3; -internal struct Sector +internal record struct Sector(uint Index, int Length) { - const int MiniSectorSize = 64; + public const int MiniSectorSize = 64; - public Sector(uint index, long length) - { - Index = index; - Length = length; - } - - public uint Index { get; } - - public long Length { get; } + public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); public readonly long StartOffset => (Index + 1) * Length; public readonly long EndOffset => (Index + 2) * Length; + + public override readonly string ToString() => $"{Index}"; } diff --git a/OpenMcdf3/SectorType.cs b/OpenMcdf3/SectorType.cs index f38feef9..f4820042 100644 --- a/OpenMcdf3/SectorType.cs +++ b/OpenMcdf3/SectorType.cs @@ -1,10 +1,10 @@ namespace OpenMcdf3; -internal enum SectorType : uint +internal static class SectorType { - Maximum = 0xFFFFFFFA, - Difat = 0xFFFFFFFC, // Specifies a DIFAT sector in the FAT. - Fat = 0xFFFFFFFD, - EndOfChain = 0xFFFFFFFE, - Free = 0xFFFFFFFF, + public const uint Maximum = 0xFFFFFFFA; + public const uint Difat = 0xFFFFFFFC; // Specifies a DIFAT sector in the FAT. + public const uint Fat = 0xFFFFFFFD; + public const uint EndOfChain = 0xFFFFFFFE; + public const uint Free = 0xFFFFFFFF; } From cacc76a4c5f184b8e47de798c8194738553a3ce4 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 14 Oct 2024 14:07:04 +1300 Subject: [PATCH 011/114] Move EntryInfo enumerator etc. to Storage --- OpenMcdf3/FatSectorChainEnumerator.cs | 2 +- OpenMcdf3/IOContext.cs | 2 +- OpenMcdf3/RootStorage.cs | 36 +++------------------------ OpenMcdf3/Storage.cs | 36 +++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index a7ea20ba..c368f8e2 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -56,6 +56,6 @@ public void Reset() public void Dispose() { - // No resources to dispose + fatEnumerator.Dispose(); } } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index abe22fbe..8ba4a857 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -6,7 +6,7 @@ internal sealed class IOContext : IDisposable public McdfBinaryReader Reader { get; } public McdfBinaryWriter? Writer { get; } - public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer = null) + public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer, bool leaveOpen = false) { Header = header; Reader = reader; diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 6792f402..5c613702 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -10,7 +10,6 @@ public enum Version : ushort public sealed class RootStorage : Storage, IDisposable { - readonly IOContext ioContext; bool disposed; public static RootStorage Create(string fileName, Version version = Version.V3) @@ -35,18 +34,18 @@ public static RootStorage OpenRead(string fileName) return Open(stream); } - public static RootStorage Open(Stream stream) + public static RootStorage Open(Stream stream, bool leaveOpen = false) { McdfBinaryReader reader = new(stream); McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); - IOContext ioContext = new(header, reader, writer); + IOContext ioContext = new(header, reader, writer, leaveOpen); return new RootStorage(ioContext); } RootStorage(IOContext ioContext) + : base(ioContext, ioContext.Header.FirstDirectorySectorID) { - this.ioContext = ioContext; } public void Dispose() @@ -54,34 +53,7 @@ public void Dispose() if (disposed) return; - ioContext.Writer?.Dispose(); - ioContext.Reader.Dispose(); + IOContext?.Dispose(); disposed = true; } - - IEnumerable EnumerateDirectoryEntries() - { - using FatSectorChainEnumerator chainEnumerator = new(ioContext, ioContext.Header.FirstDirectorySectorID); - while (chainEnumerator.MoveNext()) - { - ioContext.Reader.Seek(chainEnumerator.Current.StartOffset); - - int entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; - for (int i = 0; i < entryCount; i++) - { - DirectoryEntry entry = ioContext.Reader.ReadDirectoryEntry((Version)ioContext.Header.MajorVersion); - if (entry.Type is not StorageType.Invalid) - yield return entry; - } - } - } - - public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries().Select(e => new EntryInfo { Name = e.Name }); - - public CfbStream OpenStream(string name) - { - DirectoryEntry? entry = EnumerateDirectoryEntries() - .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); - return new CfbStream(ioContext, ioContext.Header.SectorSize, entry); - } } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index a4aa9bcc..9af23672 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -12,4 +12,40 @@ public enum StorageType public class Storage { + internal IOContext IOContext { get; } + + uint firstDirectorySector; + + internal Storage(IOContext ioContext, uint firstDirectorySector) + { + IOContext = ioContext; + this.firstDirectorySector = firstDirectorySector; + } + + IEnumerable EnumerateDirectoryEntries() + { + var version = (Version)IOContext.Header.MajorVersion; + int entryCount = IOContext.Header.SectorSize / DirectoryEntry.Length; + using FatSectorChainEnumerator chainEnumerator = new(IOContext, firstDirectorySector); + while (chainEnumerator.MoveNext()) + { + IOContext.Reader.Seek(chainEnumerator.Current.StartOffset); + + for (int i = 0; i < entryCount; i++) + { + DirectoryEntry entry = IOContext.Reader.ReadDirectoryEntry(version); + if (entry.Type is not StorageType.Invalid) + yield return entry; + } + } + } + + public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries().Select(e => new EntryInfo { Name = e.Name }); + + public CfbStream OpenStream(string name) + { + DirectoryEntry? entry = EnumerateDirectoryEntries() + .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); + return new CfbStream(IOContext, IOContext.Header.SectorSize, entry); + } } From 7821a5d148cb523d1f3c347680c5eeccc25de3e3 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 14 Oct 2024 14:11:16 +1300 Subject: [PATCH 012/114] Improve Sector validation --- OpenMcdf3/FatSectorChainEnumerator.cs | 4 ++-- OpenMcdf3/FatSectorEnumerator.cs | 7 +++---- OpenMcdf3/Sector.cs | 24 ++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index c368f8e2..b385ebe8 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -17,7 +17,7 @@ public FatSectorChainEnumerator(IOContext ioContext, uint startId) Index = SectorType.EndOfChain; this.startId = startId; this.nextId = SectorType.Free; - this.current = new Sector(0, 0); // Initialize with a default value + this.current = Sector.EndOfChain; } public uint Index { get; private set; } @@ -50,7 +50,7 @@ public void Reset() { Index = SectorType.EndOfChain; nextId = SectorType.Free; - current = new Sector(0, 0); // Reset to default value + current = Sector.EndOfChain; fatEnumerator.Reset(); } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index c34bb474..03a70903 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -15,7 +15,7 @@ public FatSectorEnumerator(IOContext ioContext) this.ioContext = ioContext; this.index = SectorType.EndOfChain; this.nextDifatSectorId = ioContext.Header.FirstDifatSectorID; - Current = default; + Current = Sector.EndOfChain; } public Sector Current { get; private set; } @@ -68,7 +68,7 @@ public void Reset() { index = SectorType.EndOfChain; difatSectorElementIndex = SectorType.EndOfChain; - Current = new Sector(0, 0); // Reset to default value + Current = Sector.EndOfChain; } public uint GetNextFatSectorId(uint id) @@ -81,8 +81,7 @@ public uint GetNextFatSectorId(uint id) return SectorType.EndOfChain; } - Sector sector = Current; - long position = sector.StartOffset + sectorOffset * sizeof(uint); + long position = Current.StartOffset + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); uint nextId = ioContext.Reader.ReadUInt32(); return nextId; diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 8974d320..4149614f 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -6,9 +6,29 @@ internal record struct Sector(uint Index, int Length) public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); - public readonly long StartOffset => (Index + 1) * Length; + readonly void ThrowIfInvalid() + { + if (Index > SectorType.Maximum) + throw new InvalidOperationException($"Invalid sector index: {Index}"); + } - public readonly long EndOffset => (Index + 2) * Length; + public readonly long StartOffset + { + get + { + ThrowIfInvalid(); + return (Index + 1) * Length; + } + } + + public readonly long EndOffset + { + get + { + ThrowIfInvalid(); + return (Index + 2) * Length; + } + } public override readonly string ToString() => $"{Index}"; } From b2fc989e4c3e6b682c8416bf5aa72b3fe37ae9e3 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 14 Oct 2024 16:24:01 +1300 Subject: [PATCH 013/114] Allow storage directory entry enumeration --- OpenMcdf3.Tests/EntryInfoTests.cs | 4 +- OpenMcdf3.Tests/MultipleStorage.cfs | Bin 0 -> 4096 bytes OpenMcdf3.Tests/MultipleStorage2.cfs | Bin 0 -> 4608 bytes OpenMcdf3.Tests/MultipleStorage3.cfs | Bin 0 -> 22016 bytes OpenMcdf3.Tests/MultipleStorage4.cfs | Bin 0 -> 53248 bytes OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 12 ++++ OpenMcdf3.Tests/StorageTests.cs | 22 +++++++ OpenMcdf3/CfbStream.cs | 6 +- OpenMcdf3/DirectoryEntry.cs | 25 ++++++-- OpenMcdf3/DirectoryEntryEnumerator.cs | 79 +++++++++++++++++++++++++ OpenMcdf3/DirectoryTreeEnumerator.cs | 55 +++++++++++++++++ OpenMcdf3/FatSectorChainEnumerator.cs | 1 + OpenMcdf3/IOContext.cs | 7 +++ OpenMcdf3/RootStorage.cs | 10 ++-- OpenMcdf3/Storage.cs | 51 +++++++--------- 15 files changed, 229 insertions(+), 43 deletions(-) create mode 100644 OpenMcdf3.Tests/MultipleStorage.cfs create mode 100644 OpenMcdf3.Tests/MultipleStorage2.cfs create mode 100644 OpenMcdf3.Tests/MultipleStorage3.cfs create mode 100644 OpenMcdf3.Tests/MultipleStorage4.cfs create mode 100644 OpenMcdf3.Tests/StorageTests.cs create mode 100644 OpenMcdf3/DirectoryEntryEnumerator.cs create mode 100644 OpenMcdf3/DirectoryTreeEnumerator.cs diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf3.Tests/EntryInfoTests.cs index 3af05877..290aec8e 100644 --- a/OpenMcdf3.Tests/EntryInfoTests.cs +++ b/OpenMcdf3.Tests/EntryInfoTests.cs @@ -4,8 +4,8 @@ public sealed class EntryInfoTests { [TestMethod] - [DataRow("_Test.ppt", 5)] - [DataRow("test.cfb", 2)] + [DataRow("_Test.ppt", 4)] + [DataRow("test.cfb", 1)] public void EnumerateEntryInfos(string fileName, int count) { using var rootStorage = RootStorage.OpenRead(fileName); diff --git a/OpenMcdf3.Tests/MultipleStorage.cfs b/OpenMcdf3.Tests/MultipleStorage.cfs new file mode 100644 index 0000000000000000000000000000000000000000..8cdb2e03caf64aae2c1d047bd047288e46d87f95 GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 009e0b55..bad7483e 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -26,6 +26,18 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs new file mode 100644 index 00000000..68695a5c --- /dev/null +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class StorageTests +{ + [TestMethod] + [DataRow("_Test.ppt", 0)] + [DataRow("MultipleStorage.cfs", 1)] + [DataRow("MultipleStorage2.cfs", 1)] + [DataRow("MultipleStorage3.cfs", 1)] + [DataRow("MultipleStorage4.cfs", 1)] + public void Read(string fileName, long storageCount) + { + using var rootStorage = RootStorage.OpenRead(fileName); + IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); + //foreach (var entryInfo in storageEntries) + // Trace.WriteLine($"{entryInfo}"); + Assert.AreEqual(storageCount, storageEntries.Count()); + } +} diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index d060d714..9ca19690 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -3,15 +3,13 @@ public class CfbStream : Stream { readonly IOContext ioContext; - readonly long sectorLength; readonly FatSectorChainEnumerator chain; long length; long position; - internal CfbStream(IOContext ioContext, long sectorLength, DirectoryEntry directoryEntry) + internal CfbStream(IOContext ioContext, DirectoryEntry directoryEntry) { this.ioContext = ioContext; - this.sectorLength = sectorLength; DirectoryEntry = directoryEntry; length = directoryEntry.StreamLength; chain = new(ioContext, directoryEntry.StartSectorLocation); @@ -40,7 +38,7 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { - int sectorIndex = (int)Math.DivRem(position, sectorLength, out long sectorOffset); + int sectorIndex = (int)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); while (sectorIndex > 0 && (chain.Index == SectorType.EndOfChain || chain.Index < sectorIndex)) { if (!chain.MoveNext()) diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 45ae5a19..127bb208 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -1,16 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices.ComTypes; -using System.Text; +using System.Text; namespace OpenMcdf3; +public enum StorageType +{ + Unallocated = 0, + Storage = 1, + Stream = 2, + Root = 5 +} + enum Color { Red = 0, Black = 1 } +internal static class StreamId +{ + public const uint Maximum = 0xFFFFFFFA; + public const uint NoStream = 0xFFFFFFFF; +} + internal sealed class DirectoryEntry { internal const int Length = 128; @@ -38,7 +49,7 @@ public string Name } } - public StorageType Type { get; set; } = StorageType.Invalid; + public StorageType Type { get; set; } = StorageType.Unallocated; public Color Color { get; set; } @@ -79,4 +90,8 @@ public DateTime ModifiedTime public uint StartSectorLocation { get; set; } public long StreamLength { get; set; } + + public EntryInfo ToEntryInfo() => new() { Name = Name }; + + public override string ToString() => Name; } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs new file mode 100644 index 00000000..aec02b47 --- /dev/null +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -0,0 +1,79 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal sealed class DirectoryEntryEnumerator : IEnumerator +{ + private readonly IOContext ioContext; + private readonly Version version; + private readonly int entryCount; + private readonly FatSectorChainEnumerator chainEnumerator; + private int entryIndex; + private DirectoryEntry? current; + + public DirectoryEntryEnumerator(IOContext ioContext) + { + this.ioContext = ioContext; + this.version = (Version)ioContext.Header.MajorVersion; + this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; + this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorID); + this.entryIndex = -1; + this.current = default; + } + + public DirectoryEntry Current => current!; + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (entryIndex == -1 || entryIndex >= entryCount) + { + if (!chainEnumerator.MoveNext()) + return false; + + ioContext.Reader.Seek(chainEnumerator.Current.StartOffset); + entryIndex = 0; + } + + current = ioContext.Reader.ReadDirectoryEntry(version); + entryIndex++; + return current.Type != StorageType.Unallocated; + } + + public DirectoryEntry? Get(uint id) + { + if (id == StreamId.NoStream) + return null; + + int sectorIndex = Math.DivRem((int)id, entryCount, out int entryIndex); + if (sectorIndex < chainEnumerator.Index) + { + chainEnumerator.Reset(); + chainEnumerator.MoveNext(); + } + + while (chainEnumerator.Index - 1 < sectorIndex) + { + if (!chainEnumerator.MoveNext()) + return null; + } + + long position = chainEnumerator.Current.StartOffset + entryIndex * DirectoryEntry.Length; + ioContext.Reader.Seek(position); + current = ioContext.Reader.ReadDirectoryEntry(version); + return current; + } + + public void Reset() + { + chainEnumerator.Reset(); + entryIndex = -1; + current = default!; + } + + public void Dispose() + { + chainEnumerator.Dispose(); + } +} diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs new file mode 100644 index 00000000..631cfd15 --- /dev/null +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -0,0 +1,55 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal sealed class DirectoryTreeEnumerator : IEnumerator +{ + private readonly DirectoryEntry? child; + private readonly Stack stack = new(); + private readonly DirectoryEntryEnumerator directoryEntryEnumerator; + DirectoryEntry current; + + internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) + { + directoryEntryEnumerator = new(ioContext); + this.child = directoryEntryEnumerator.Get(root.ChildID); + PushLeft(child); + current = default!; + } + + public void Dispose() + { + directoryEntryEnumerator.Dispose(); + } + + public DirectoryEntry Current => current; + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (stack.Count == 0) + return false; + + current = stack.Pop(); + DirectoryEntry? rightSibling = directoryEntryEnumerator.Get(Current.RightSiblingID); + PushLeft(rightSibling); + return true; + } + + public void Reset() + { + current = default!; + stack.Clear(); + PushLeft(child); + } + + private void PushLeft(DirectoryEntry? node) + { + while (node is not null) + { + stack.Push(node); + node = directoryEntryEnumerator.Get(node.LeftSiblingID); + } + } +} diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index b385ebe8..f0a72af6 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -20,6 +20,7 @@ public FatSectorChainEnumerator(IOContext ioContext, uint startId) this.current = Sector.EndOfChain; } + // TODO: Fix off-by one error public uint Index { get; private set; } public Sector Current => current; diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 8ba4a857..b59774f3 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -18,4 +18,11 @@ public void Dispose() Reader.Dispose(); Writer?.Dispose(); } + + public IEnumerable EnumerateDirectoryEntries() + { + using DirectoryEntryEnumerator directoryEntriesEnumerator = new(this); + while (directoryEntriesEnumerator.MoveNext()) + yield return directoryEntriesEnumerator.Current; + } } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 5c613702..d3a4cf4f 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -19,7 +19,8 @@ public static RootStorage Create(string fileName, Version version = Version.V3) McdfBinaryReader reader = new(stream); McdfBinaryWriter writer = new(stream); IOContext ioContext = new(header, reader, writer); - return new RootStorage(ioContext); + DirectoryEntry directoryEntry = new(); + return new RootStorage(ioContext, directoryEntry); } public static RootStorage Open(string fileName, FileMode mode) @@ -40,11 +41,12 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); IOContext ioContext = new(header, reader, writer, leaveOpen); - return new RootStorage(ioContext); + DirectoryEntry rootDirectoryEntry = ioContext.EnumerateDirectoryEntries().First(); + return new RootStorage(ioContext, rootDirectoryEntry); } - RootStorage(IOContext ioContext) - : base(ioContext, ioContext.Header.FirstDirectorySectorID) + RootStorage(IOContext ioContext, DirectoryEntry rootDirectoryEntry) + : base(ioContext, rootDirectoryEntry) { } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 9af23672..cee6f926 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -1,51 +1,46 @@ namespace OpenMcdf3; -public enum StorageType -{ - Invalid = 0, - Storage = 1, - Stream = 2, - Lockbytes = 3, - Property = 4, - Root = 5 -} - public class Storage { internal IOContext IOContext { get; } - uint firstDirectorySector; + internal DirectoryEntry DirectoryEntry { get; } - internal Storage(IOContext ioContext, uint firstDirectorySector) + internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) { IOContext = ioContext; - this.firstDirectorySector = firstDirectorySector; + DirectoryEntry = directoryEntry; } + public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries() + .Select(e => e.ToEntryInfo()); + + public IEnumerable EnumerateEntries(StorageType type) => EnumerateDirectoryEntries(type) + .Select(e => e.ToEntryInfo()); + IEnumerable EnumerateDirectoryEntries() { - var version = (Version)IOContext.Header.MajorVersion; - int entryCount = IOContext.Header.SectorSize / DirectoryEntry.Length; - using FatSectorChainEnumerator chainEnumerator = new(IOContext, firstDirectorySector); - while (chainEnumerator.MoveNext()) + using DirectoryTreeEnumerator treeEnumerator = new(IOContext, DirectoryEntry); + while (treeEnumerator.MoveNext()) { - IOContext.Reader.Seek(chainEnumerator.Current.StartOffset); - - for (int i = 0; i < entryCount; i++) - { - DirectoryEntry entry = IOContext.Reader.ReadDirectoryEntry(version); - if (entry.Type is not StorageType.Invalid) - yield return entry; - } + yield return treeEnumerator.Current; } } - public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries().Select(e => new EntryInfo { Name = e.Name }); + IEnumerable EnumerateDirectoryEntries(StorageType type) => EnumerateDirectoryEntries() + .Where(e => e.Type == type); + + public Storage OpenStorage(string name) + { + DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Storage) + .FirstOrDefault(entry => entry.Name == name) ?? throw new DirectoryNotFoundException($"Directory not found {name}"); + return new Storage(IOContext, entry); + } public CfbStream OpenStream(string name) { - DirectoryEntry? entry = EnumerateDirectoryEntries() + DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); - return new CfbStream(IOContext, IOContext.Header.SectorSize, entry); + return new CfbStream(IOContext, entry); } } From 4d511e09962e72e130362a1751312505f5491e9f Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 14 Oct 2024 21:30:01 +1300 Subject: [PATCH 014/114] Improve enumerator validation --- OpenMcdf3.Tests/CfbStreamTests.cs | 17 ++++- OpenMcdf3/CfbStream.cs | 20 +++--- OpenMcdf3/DirectoryEntryEnumerator.cs | 29 ++++----- OpenMcdf3/DirectoryTreeEnumerator.cs | 20 ++++-- OpenMcdf3/FatSectorChainEnumerator.cs | 69 +++++++++++++------- OpenMcdf3/FatSectorEnumerator.cs | 91 ++++++++++++++++----------- OpenMcdf3/Sector.cs | 26 +++++--- 7 files changed, 176 insertions(+), 96 deletions(-) diff --git a/OpenMcdf3.Tests/CfbStreamTests.cs b/OpenMcdf3.Tests/CfbStreamTests.cs index 037f99c9..1035ac00 100644 --- a/OpenMcdf3.Tests/CfbStreamTests.cs +++ b/OpenMcdf3.Tests/CfbStreamTests.cs @@ -6,7 +6,7 @@ public sealed class CfbStreamTests [TestMethod] [DataRow("_Test.ppt", "Current User", 62)] [DataRow("test.cfb", "MyStream0", 1048576)] - public void CfbStreamTest(string fileName, string streamName, long length) + public void Read(string fileName, string streamName, long length) { using var rootStorage = RootStorage.OpenRead(fileName); using CfbStream stream = rootStorage.OpenStream(streamName); @@ -16,4 +16,19 @@ public void CfbStreamTest(string fileName, string streamName, long length) stream.CopyTo(memoryStream); Assert.AreEqual(length, memoryStream.Length); } + + [TestMethod] + [DataRow("_Test.ppt")] + [DataRow("test.cfb")] + public void ReadAllStreams(string fileName) + { + using var rootStorage = RootStorage.OpenRead(fileName); + foreach (EntryInfo entryInfo in rootStorage.EnumerateEntries(StorageType.Stream)) + { + using CfbStream stream = rootStorage.OpenStream(entryInfo.Name); + using MemoryStream memoryStream = new(); + stream.CopyTo(memoryStream); + //Assert.AreEqual(entryInfo.Length, memoryStream.Length); + } + } } diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index 9ca19690..768533d6 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -31,6 +31,13 @@ public override long Position set => position = value; } + protected override void Dispose(bool disposing) + { + chain.Dispose(); + + base.Dispose(disposing); + } + public override void Flush() { //rootStorage.Flush(); @@ -38,18 +45,15 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { - int sectorIndex = (int)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); - while (sectorIndex > 0 && (chain.Index == SectorType.EndOfChain || chain.Index < sectorIndex)) - { - if (!chain.MoveNext()) - return 0; - } + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + return 0; int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); int realCount = Math.Min(count, maxCount); int readCount = 0; int remaining = realCount; - while (chain.MoveNext()) + do { Sector sector = chain.Current; long readLength = Math.Min(remaining, sector.Length - sectorOffset); @@ -61,7 +65,7 @@ public override int Read(byte[] buffer, int offset, int count) readCount += read; if (readCount >= realCount) return readCount; - } + } while (chain.MoveNext()); return readCount; } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index aec02b47..655cbfd1 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -8,7 +8,7 @@ internal sealed class DirectoryEntryEnumerator : IEnumerator private readonly Version version; private readonly int entryCount; private readonly FatSectorChainEnumerator chainEnumerator; - private int entryIndex; + private int entryIndex = -1; private DirectoryEntry? current; public DirectoryEntryEnumerator(IOContext ioContext) @@ -17,11 +17,17 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.version = (Version)ioContext.Header.MajorVersion; this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorID); - this.entryIndex = -1; - this.current = default; } - public DirectoryEntry Current => current!; + public DirectoryEntry Current + { + get + { + if (current is null) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } object IEnumerator.Current => Current; @@ -46,18 +52,9 @@ public bool MoveNext() if (id == StreamId.NoStream) return null; - int sectorIndex = Math.DivRem((int)id, entryCount, out int entryIndex); - if (sectorIndex < chainEnumerator.Index) - { - chainEnumerator.Reset(); - chainEnumerator.MoveNext(); - } - - while (chainEnumerator.Index - 1 < sectorIndex) - { - if (!chainEnumerator.MoveNext()) - return null; - } + uint chainIndex = (uint)Math.DivRem(id, entryCount, out long entryIndex); + if (!chainEnumerator.MoveTo(chainIndex)) + throw new ArgumentException("Invalid directory entry ID"); long position = chainEnumerator.Current.StartOffset + entryIndex * DirectoryEntry.Length; ioContext.Reader.Seek(position); diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 631cfd15..ab1aed59 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -7,14 +7,13 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator private readonly DirectoryEntry? child; private readonly Stack stack = new(); private readonly DirectoryEntryEnumerator directoryEntryEnumerator; - DirectoryEntry current; + DirectoryEntry? current; internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); - this.child = directoryEntryEnumerator.Get(root.ChildID); + child = directoryEntryEnumerator.Get(root.ChildID); PushLeft(child); - current = default!; } public void Dispose() @@ -22,14 +21,25 @@ public void Dispose() directoryEntryEnumerator.Dispose(); } - public DirectoryEntry Current => current; + public DirectoryEntry Current + { + get + { + if (current is null) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } object IEnumerator.Current => Current; public bool MoveNext() { if (stack.Count == 0) + { + current = null; return false; + } current = stack.Pop(); DirectoryEntry? rightSibling = directoryEntryEnumerator.Get(Current.RightSiblingID); @@ -39,7 +49,7 @@ public bool MoveNext() public void Reset() { - current = default!; + current = null; stack.Clear(); PushLeft(child); } diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index f0a72af6..bad0f6e5 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -4,55 +4,80 @@ namespace OpenMcdf3; internal sealed class FatSectorChainEnumerator : IEnumerator { - private readonly FatSectorEnumerator fatEnumerator; private readonly IOContext ioContext; + private readonly FatSectorEnumerator fatEnumerator; private readonly uint startId; - private uint nextId; - private Sector current; + private bool start = true; + private Sector current = Sector.EndOfChain; - public FatSectorChainEnumerator(IOContext ioContext, uint startId) + public FatSectorChainEnumerator(IOContext ioContext, uint startSectorId) { - fatEnumerator = new(ioContext); this.ioContext = ioContext; - Index = SectorType.EndOfChain; - this.startId = startId; - this.nextId = SectorType.Free; - this.current = Sector.EndOfChain; + if (startSectorId is SectorType.EndOfChain) + throw new ArgumentException("Invalid start sector ID", nameof(startSectorId)); + this.startId = startSectorId; + fatEnumerator = new(ioContext); } - // TODO: Fix off-by one error - public uint Index { get; private set; } + public uint Index { get; private set; } = uint.MaxValue; - public Sector Current => current; + public Sector Current + { + get + { + if (current.IsEndOfChain) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } object IEnumerator.Current => Current; public bool MoveNext() { - if (nextId is SectorType.Free) + if (start) { + current = new(startId, ioContext.Header.SectorSize); Index = 0; - nextId = startId; + start = false; + } + else if (!current.IsEndOfChain) + { + uint sectorId = fatEnumerator.GetNextFatSectorId(current.Id); + current = new(sectorId, ioContext.Header.SectorSize); + Index++; } - if (nextId is SectorType.EndOfChain) + if (current.IsEndOfChain) { - Index = SectorType.EndOfChain; + current = Sector.EndOfChain; + Index = uint.MaxValue; return false; } - Index++; - current = new Sector(nextId, ioContext.Header.SectorSize); - nextId = fatEnumerator.GetNextFatSectorId(nextId); + return true; + } + + public bool MoveTo(uint index) + { + if (index < Index) + Reset(); + + while (start || Index < index) + { + if (!MoveNext()) + return false; + } + return true; } public void Reset() { - Index = SectorType.EndOfChain; - nextId = SectorType.Free; - current = Sector.EndOfChain; + start = true; fatEnumerator.Reset(); + current = Sector.EndOfChain; + Index = uint.MaxValue; } public void Dispose() diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 03a70903..97430152 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using System.Diagnostics.SymbolStore; namespace OpenMcdf3; @@ -6,80 +7,100 @@ namespace OpenMcdf3; internal sealed class FatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; - private uint index = SectorType.EndOfChain; - uint nextDifatSectorId = SectorType.EndOfChain; - uint difatSectorElementIndex = SectorType.EndOfChain; + private bool start = true; + private uint id = SectorType.EndOfChain; + private uint difatSectorId; + private uint difatSectorElementIndex = 0; + private Sector current = Sector.EndOfChain; public FatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - this.index = SectorType.EndOfChain; - this.nextDifatSectorId = ioContext.Header.FirstDifatSectorID; - Current = Sector.EndOfChain; + this.difatSectorId = ioContext.Header.FirstDifatSectorID; } - public Sector Current { get; private set; } + public Sector Current + { + get + { + if (current.IsEndOfChain) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } object IEnumerator.Current => Current; public bool MoveNext() { - if (index == SectorType.EndOfChain) + if (start) { - index = 0; + id = uint.MaxValue; + start = false; } - if (index < ioContext.Header.FatSectorCount && index < Header.DifatLength) + id++; + + if (id < ioContext.Header.FatSectorCount && id < Header.DifatLength) { - uint nextId = ioContext.Header.Difat[index]; - Current = new Sector(nextId, ioContext.Header.SectorSize); - index++; + uint id = ioContext.Header.Difat[this.id]; + current = new Sector(id, ioContext.Header.SectorSize); return true; } - if (nextDifatSectorId == SectorType.EndOfChain) + if (difatSectorId == SectorType.EndOfChain) + { + current = Sector.EndOfChain; + id = SectorType.EndOfChain; return false; + } int difatElementCount = ioContext.Header.SectorSize / sizeof(uint) - 1; - Sector difatSector = new(nextDifatSectorId, ioContext.Header.SectorSize); + Sector difatSector = new(difatSectorId, ioContext.Header.SectorSize); + long position = difatSector.StartOffset + difatSectorElementIndex * sizeof(uint); + ioContext.Reader.Seek(position); + uint sectorId = ioContext.Reader.ReadUInt32(); + current = new Sector(sectorId, ioContext.Header.SectorSize); + difatSectorElementIndex++; + id++; + if (difatSectorElementIndex == difatElementCount) { - long nextIdOffset = difatSector.EndOffset - sizeof(uint); - ioContext.Reader.Seek(nextIdOffset); - nextDifatSectorId = ioContext.Reader.ReadUInt32(); + difatSectorId = ioContext.Reader.ReadUInt32(); difatSectorElementIndex = 0; } - if (difatSectorElementIndex < difatElementCount) + return true; + } + + public bool MoveTo(uint sectorId) + { + if (sectorId < id) + Reset(); + + while (start || id < sectorId) { - long position = difatSector.StartOffset + difatSectorElementIndex * sizeof(uint); - ioContext.Reader.Seek(position); - uint nextId = ioContext.Reader.ReadUInt32(); - Current = new Sector(nextId, ioContext.Header.SectorSize); - difatSectorElementIndex++; - index++; - return true; + if (!MoveNext()) + return false; } - return false; + return true; } public void Reset() { - index = SectorType.EndOfChain; + start = true; + id = SectorType.EndOfChain; difatSectorElementIndex = SectorType.EndOfChain; - Current = Sector.EndOfChain; + current = Sector.EndOfChain; } public uint GetNextFatSectorId(uint id) { int elementLength = ioContext.Header.SectorSize / sizeof(uint); - int sectorId = (int)Math.DivRem(id, elementLength, out long sectorOffset); - while (index == SectorType.EndOfChain || index - 1 < sectorId) - { - if (!MoveNext()) - return SectorType.EndOfChain; - } + uint sectorId = (uint)Math.DivRem(id, elementLength, out long sectorOffset); + if (!MoveTo(sectorId)) + throw new ArgumentException("Invalid sector ID"); long position = Current.StartOffset + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 4149614f..10c956f9 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -1,23 +1,25 @@ namespace OpenMcdf3; -internal record struct Sector(uint Index, int Length) +internal record struct Sector(uint Id, int Length) { public const int MiniSectorSize = 64; public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); - readonly void ThrowIfInvalid() - { - if (Index > SectorType.Maximum) - throw new InvalidOperationException($"Invalid sector index: {Index}"); - } + /// + /// Compound File Binary File Format only specifies that ENDOFCHAIN ends the DIFAT chain + /// but some implementations use FREESECT + /// + public readonly bool IsEndOfChain => Id is SectorType.EndOfChain or SectorType.Free; + + public readonly bool IsValid => Id <= SectorType.Maximum; public readonly long StartOffset { get { ThrowIfInvalid(); - return (Index + 1) * Length; + return (Id + 1) * Length; } } @@ -26,9 +28,15 @@ public readonly long EndOffset get { ThrowIfInvalid(); - return (Index + 2) * Length; + return (Id + 2) * Length; } } - public override readonly string ToString() => $"{Index}"; + readonly void ThrowIfInvalid() + { + if (!IsValid) + throw new InvalidOperationException($"Invalid sector index: {Id}"); + } + + public override readonly string ToString() => $"{Id}"; } From 6c13bec8230f1d73a4bbd64ac0d522af90011ac8 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 16 Oct 2024 09:41:18 +1300 Subject: [PATCH 015/114] Read mini FAT sectors, chains and streams --- OpenMcdf3.Benchmarks/InMemory.cs | 2 +- OpenMcdf3.Tests/CfbStreamTests.cs | 5 +- OpenMcdf3/DirectoryEntry.cs | 1 + OpenMcdf3/{CfbStream.cs => FatStream.cs} | 4 +- OpenMcdf3/IOContext.cs | 1 + OpenMcdf3/MiniFatSectorChainEnumerator.cs | 85 ++++++++++++++++++++++ OpenMcdf3/MiniFatSectorEnumerator.cs | 81 +++++++++++++++++++++ OpenMcdf3/MiniFatStream.cs | 89 +++++++++++++++++++++++ OpenMcdf3/MiniSector.cs | 38 ++++++++++ OpenMcdf3/RootStorage.cs | 16 ++-- OpenMcdf3/Sector.cs | 2 - OpenMcdf3/Storage.cs | 7 +- 12 files changed, 314 insertions(+), 17 deletions(-) rename OpenMcdf3/{CfbStream.cs => FatStream.cs} (95%) create mode 100644 OpenMcdf3/MiniFatSectorChainEnumerator.cs create mode 100644 OpenMcdf3/MiniFatSectorEnumerator.cs create mode 100644 OpenMcdf3/MiniFatStream.cs create mode 100644 OpenMcdf3/MiniSector.cs diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index c33a02f1..96bd180c 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -57,7 +57,7 @@ public void Test() _stream.Seek(0L, SeekOrigin.Begin); // using var compoundFile = RootStorage.Open(_stream); - using CfbStream cfStream = compoundFile.OpenStream(streamName + 0); + using Stream cfStream = compoundFile.OpenStream(streamName + 0); long streamSize = cfStream.Length; long position = 0L; while (true) diff --git a/OpenMcdf3.Tests/CfbStreamTests.cs b/OpenMcdf3.Tests/CfbStreamTests.cs index 1035ac00..ff40b79f 100644 --- a/OpenMcdf3.Tests/CfbStreamTests.cs +++ b/OpenMcdf3.Tests/CfbStreamTests.cs @@ -9,7 +9,7 @@ public sealed class CfbStreamTests public void Read(string fileName, string streamName, long length) { using var rootStorage = RootStorage.OpenRead(fileName); - using CfbStream stream = rootStorage.OpenStream(streamName); + using Stream stream = rootStorage.OpenStream(streamName); Assert.AreEqual(length, stream.Length); using MemoryStream memoryStream = new(); @@ -25,10 +25,9 @@ public void ReadAllStreams(string fileName) using var rootStorage = RootStorage.OpenRead(fileName); foreach (EntryInfo entryInfo in rootStorage.EnumerateEntries(StorageType.Stream)) { - using CfbStream stream = rootStorage.OpenStream(entryInfo.Name); + using Stream stream = rootStorage.OpenStream(entryInfo.Name); using MemoryStream memoryStream = new(); stream.CopyTo(memoryStream); - //Assert.AreEqual(entryInfo.Length, memoryStream.Length); } } } diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 127bb208..d28b07a2 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -87,6 +87,7 @@ public DateTime ModifiedTime } } + // Also the first sector of the mini-stream for the root storage object public uint StartSectorLocation { get; set; } public long StreamLength { get; set; } diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/FatStream.cs similarity index 95% rename from OpenMcdf3/CfbStream.cs rename to OpenMcdf3/FatStream.cs index 768533d6..702ff983 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -1,13 +1,13 @@ namespace OpenMcdf3; -public class CfbStream : Stream +public class FatStream : Stream { readonly IOContext ioContext; readonly FatSectorChainEnumerator chain; long length; long position; - internal CfbStream(IOContext ioContext, DirectoryEntry directoryEntry) + internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) { this.ioContext = ioContext; DirectoryEntry = directoryEntry; diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index b59774f3..9be8a72b 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -5,6 +5,7 @@ internal sealed class IOContext : IDisposable public Header Header { get; } public McdfBinaryReader Reader { get; } public McdfBinaryWriter? Writer { get; } + public DirectoryEntry RootEntry { get; set; } public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer, bool leaveOpen = false) { diff --git a/OpenMcdf3/MiniFatSectorChainEnumerator.cs b/OpenMcdf3/MiniFatSectorChainEnumerator.cs new file mode 100644 index 00000000..d406e1bc --- /dev/null +++ b/OpenMcdf3/MiniFatSectorChainEnumerator.cs @@ -0,0 +1,85 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal sealed class MiniFatSectorChainEnumerator : IEnumerator +{ + private readonly MiniFatSectorEnumerator miniFatEnumerator; + private readonly uint startId; + private bool start = true; + private MiniSector current = MiniSector.EndOfChain; + + public MiniFatSectorChainEnumerator(IOContext ioContext, uint startSectorId) + { + if (startSectorId is SectorType.EndOfChain) + throw new ArgumentException("Invalid start sector ID", nameof(startSectorId)); + this.startId = startSectorId; + miniFatEnumerator = new(ioContext); + } + + public uint Index { get; private set; } = uint.MaxValue; + + public MiniSector Current + { + get + { + if (current.IsEndOfChain) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (start) + { + current = new(startId); + Index = 0; + start = false; + } + else if (!current.IsEndOfChain) + { + uint sectorId = miniFatEnumerator.GetNextMiniFatSectorId(current.Id); + current = new(sectorId); + Index++; + } + + if (current.IsEndOfChain) + { + current = MiniSector.EndOfChain; + Index = uint.MaxValue; + return false; + } + + return true; + } + + public bool MoveTo(uint index) + { + if (index < Index) + Reset(); + + while (start || Index < index) + { + if (!MoveNext()) + return false; + } + + return true; + } + + public void Reset() + { + start = true; + miniFatEnumerator.Reset(); + current = MiniSector.EndOfChain; + Index = uint.MaxValue; + } + + public void Dispose() + { + miniFatEnumerator.Dispose(); + } +} diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs new file mode 100644 index 00000000..9a516cbc --- /dev/null +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -0,0 +1,81 @@ +using System.Collections; + +namespace OpenMcdf3; + +internal sealed class MiniFatSectorEnumerator : IEnumerator +{ + private readonly IOContext ioContext; + private readonly FatSectorChainEnumerator miniFatChain; + bool start = true; + MiniSector current = MiniSector.EndOfChain; + + public MiniFatSectorEnumerator(IOContext ioContext) + { + this.ioContext = ioContext; + miniFatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorID); + } + + public MiniSector Current + { + get + { + if (current.IsEndOfChain) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (start) + { + current = new(ioContext.Header.FirstMiniFatSectorID); + start = false; + } + else if (!current.IsEndOfChain) + { + uint sectorId = GetNextMiniFatSectorId(current.Id); + current = new(sectorId); + } + + return !current.IsEndOfChain; + } + + public bool MoveTo(uint id) + { + if (id == SectorType.EndOfChain) + return false; + + while (start || current.Id < id) + { + if (!MoveNext()) + return false; + } + + return true; + } + + public void Reset() + { + current = MiniSector.EndOfChain; + } + + public uint GetNextMiniFatSectorId(uint id) + { + int elementLength = ioContext.Header.SectorSize / sizeof(uint); + uint sectorId = (uint)Math.DivRem(id, elementLength, out long sectorOffset); + if (!miniFatChain.MoveTo(sectorId)) + throw new ArgumentException("Invalid sector ID"); + + long position = miniFatChain.Current.StartOffset + sectorOffset * sizeof(uint); + ioContext.Reader.Seek(position); + uint nextId = ioContext.Reader.ReadUInt32(); + return nextId; + } + + public void Dispose() + { + } +} diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs new file mode 100644 index 00000000..a7a7f26e --- /dev/null +++ b/OpenMcdf3/MiniFatStream.cs @@ -0,0 +1,89 @@ +namespace OpenMcdf3; + +public class MiniFatStream : Stream +{ + readonly IOContext ioContext; + readonly MiniFatSectorChainEnumerator chain; + readonly FatStream fatStream; + long length; + long position; + + internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) + { + this.ioContext = ioContext; + DirectoryEntry = directoryEntry; + length = directoryEntry.StreamLength; + chain = new(ioContext, directoryEntry.StartSectorLocation); + fatStream = new(ioContext, ioContext.RootEntry); + } + + internal DirectoryEntry DirectoryEntry { get; private set; } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => length; + + public override long Position + { + get => position; + set => position = value; + } + + protected override void Dispose(bool disposing) + { + chain.Dispose(); + fatStream.Dispose(); + + base.Dispose(disposing); + } + + public override void Flush() + { + fatStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + return 0; + + int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + int realCount = Math.Min(count, maxCount); + int readCount = 0; + do + { + MiniSector sector = chain.Current; + int remaining = realCount - readCount; + long readLength = Math.Min(remaining, MiniSector.Length - sectorOffset); + fatStream.Position = sector.StartOffset + sectorOffset; + int read = fatStream.Read(buffer, offset + readCount, (int)readLength); + if (read == 0) + return 0; + position += read; + readCount += read; + sectorOffset = 0; + if (readCount >= realCount) + return readCount; + } while (chain.MoveNext()); + + return readCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + position = offset; + return position; + } + + public override void SetLength(long value) + { + length = value; + } + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs new file mode 100644 index 00000000..bf236065 --- /dev/null +++ b/OpenMcdf3/MiniSector.cs @@ -0,0 +1,38 @@ +namespace OpenMcdf3; + +internal record struct MiniSector(uint Id) +{ + public const int Length = 64; + + public static readonly MiniSector EndOfChain = new(SectorType.EndOfChain); + + public readonly bool IsValid => Id <= SectorType.Maximum; + + public readonly bool IsEndOfChain => Id is SectorType.EndOfChain or SectorType.Free; + + public readonly long StartOffset + { + get + { + ThrowIfInvalid(); + return Id * Length; + } + } + + public readonly long EndOffset + { + get + { + ThrowIfInvalid(); + return (Id + 1) * Length; + } + } + + readonly void ThrowIfInvalid() + { + if (!IsValid) + throw new InvalidOperationException($"Invalid sector index: {Id}"); + } + + public override readonly string ToString() => $"{Id}"; +} diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index d3a4cf4f..8ed3fe38 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -18,9 +18,11 @@ public static RootStorage Create(string fileName, Version version = Version.V3) Header header = new(version); McdfBinaryReader reader = new(stream); McdfBinaryWriter writer = new(stream); - IOContext ioContext = new(header, reader, writer); - DirectoryEntry directoryEntry = new(); - return new RootStorage(ioContext, directoryEntry); + IOContext ioContext = new(header, reader, writer) + { + RootEntry = new() + }; + return new RootStorage(ioContext); } public static RootStorage Open(string fileName, FileMode mode) @@ -41,12 +43,12 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); IOContext ioContext = new(header, reader, writer, leaveOpen); - DirectoryEntry rootDirectoryEntry = ioContext.EnumerateDirectoryEntries().First(); - return new RootStorage(ioContext, rootDirectoryEntry); + ioContext.RootEntry = ioContext.EnumerateDirectoryEntries().First(); + return new RootStorage(ioContext); } - RootStorage(IOContext ioContext, DirectoryEntry rootDirectoryEntry) - : base(ioContext, rootDirectoryEntry) + RootStorage(IOContext ioContext) + : base(ioContext, ioContext.RootEntry) { } diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 10c956f9..6a22ab8b 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -2,8 +2,6 @@ internal record struct Sector(uint Id, int Length) { - public const int MiniSectorSize = 64; - public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); /// diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index cee6f926..b00bc73e 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -37,10 +37,13 @@ public Storage OpenStorage(string name) return new Storage(IOContext, entry); } - public CfbStream OpenStream(string name) + public Stream OpenStream(string name) { DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); - return new CfbStream(IOContext, entry); + if (entry.StreamLength < Header.MiniStreamCutoffSize) + return new MiniFatStream(IOContext, entry); + else + return new FatStream(IOContext, entry); } } From 9a8738b854f1a29b6a8e3e60e484ff1f8e27bf2a Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 16 Oct 2024 13:40:29 +1300 Subject: [PATCH 016/114] Improve stream/storage validation --- OpenMcdf3/EntryInfo.cs | 2 +- OpenMcdf3/FatSectorChainEnumerator.cs | 2 - OpenMcdf3/FatSectorEnumerator.cs | 2 - OpenMcdf3/FatStream.cs | 75 ++++++++++++++++++----- OpenMcdf3/IOContext.cs | 29 +++++++-- OpenMcdf3/MiniFatSectorChainEnumerator.cs | 2 - OpenMcdf3/MiniFatStream.cs | 72 ++++++++++++++++++---- OpenMcdf3/RootStorage.cs | 24 ++------ OpenMcdf3/Storage.cs | 36 ++++++++--- OpenMcdf3/ThrowHelper.cs | 16 +++++ 10 files changed, 191 insertions(+), 69 deletions(-) create mode 100644 OpenMcdf3/ThrowHelper.cs diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs index 8904164a..c22a137b 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf3/EntryInfo.cs @@ -2,7 +2,7 @@ public class EntryInfo { - public string Name { get; internal set; } + public string Name { get; internal set; } = string.Empty; public override string ToString() => Name; } diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index bad0f6e5..371c0016 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -13,8 +13,6 @@ internal sealed class FatSectorChainEnumerator : IEnumerator public FatSectorChainEnumerator(IOContext ioContext, uint startSectorId) { this.ioContext = ioContext; - if (startSectorId is SectorType.EndOfChain) - throw new ArgumentException("Invalid start sector ID", nameof(startSectorId)); this.startId = startSectorId; fatEnumerator = new(ioContext); } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 97430152..72258cf0 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -1,6 +1,4 @@ using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.SymbolStore; namespace OpenMcdf3; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 702ff983..b47b4754 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -1,11 +1,12 @@ namespace OpenMcdf3; -public class FatStream : Stream +internal class FatStream : Stream { readonly IOContext ioContext; readonly FatSectorChainEnumerator chain; - long length; + readonly long length; long position; + bool disposed; internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) { @@ -28,41 +29,61 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) public override long Position { get => position; - set => position = value; + set => Seek(value, SeekOrigin.Begin); } protected override void Dispose(bool disposing) { - chain.Dispose(); + if (!disposed) + { + chain.Dispose(); + disposed = true; + } base.Dispose(disposing); } - public override void Flush() - { - //rootStorage.Flush(); - } + public override void Flush() => this.ThrowIfDisposed(disposed); public override int Read(byte[] buffer, int offset, int count) { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number"); + + if ((uint)count > buffer.Length - offset) + 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"); + + this.ThrowIfDisposed(disposed); + + if (count == 0) + return 0; + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + if (maxCount == 0) + return 0; + int realCount = Math.Min(count, maxCount); int readCount = 0; - int remaining = realCount; do { Sector sector = chain.Current; + int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); ioContext.Reader.Seek(sector.StartOffset + sectorOffset); - int read = ioContext.Reader.Read(buffer, offset, (int)readLength); + int localOffset = offset + readCount; + int read = ioContext.Reader.Read(buffer, localOffset, (int)readLength); if (read == 0) - return 0; + return readCount; position += read; readCount += read; + sectorOffset = 0; if (readCount >= realCount) return readCount; } while (chain.MoveNext()); @@ -72,14 +93,36 @@ public override int Read(byte[] buffer, int offset, int count) public override long Seek(long offset, SeekOrigin origin) { - position = offset; + this.ThrowIfDisposed(disposed); + + switch (origin) + { + case SeekOrigin.Begin: + if (offset < 0) + throw new IOException("Seek before origin"); + position = offset; + break; + + case SeekOrigin.Current: + if (position + offset < 0) + throw new IOException("Seek before origin"); + position += offset; + break; + + case SeekOrigin.End: + if (Length - offset < 0) + throw new IOException("Seek before origin"); + position = Length - offset; + break; + + default: + throw new ArgumentException(nameof(origin), "Invalid seek origin"); + } + return position; } - public override void SetLength(long value) - { - length = value; - } + public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 9be8a72b..e70f4f15 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -1,27 +1,48 @@ namespace OpenMcdf3; +enum IOContextFlags +{ + None = 0, + Create = 1, + LeaveOpen = 2 +} + internal sealed class IOContext : IDisposable { public Header Header { get; } + public McdfBinaryReader Reader { get; } + public McdfBinaryWriter? Writer { get; } - public DirectoryEntry RootEntry { get; set; } - public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer, bool leaveOpen = false) + public DirectoryEntry RootEntry { get; } + + public bool IsDisposed { get; private set; } + + public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer, IOContextFlags contextFlags = IOContextFlags.None) { Header = header; Reader = reader; Writer = writer; + RootEntry = contextFlags.HasFlag(IOContextFlags.Create) + ? new DirectoryEntry() + : EnumerateDirectoryEntries().First(); } public void Dispose() { - Reader.Dispose(); - Writer?.Dispose(); + if (!IsDisposed) + { + Reader.Dispose(); + Writer?.Dispose(); + IsDisposed = true; + } } public IEnumerable EnumerateDirectoryEntries() { + this.ThrowIfDisposed(IsDisposed); + using DirectoryEntryEnumerator directoryEntriesEnumerator = new(this); while (directoryEntriesEnumerator.MoveNext()) yield return directoryEntriesEnumerator.Current; diff --git a/OpenMcdf3/MiniFatSectorChainEnumerator.cs b/OpenMcdf3/MiniFatSectorChainEnumerator.cs index d406e1bc..b1296c16 100644 --- a/OpenMcdf3/MiniFatSectorChainEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorChainEnumerator.cs @@ -11,8 +11,6 @@ internal sealed class MiniFatSectorChainEnumerator : IEnumerator public MiniFatSectorChainEnumerator(IOContext ioContext, uint startSectorId) { - if (startSectorId is SectorType.EndOfChain) - throw new ArgumentException("Invalid start sector ID", nameof(startSectorId)); this.startId = startSectorId; miniFatEnumerator = new(ioContext); } diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index a7a7f26e..ce081f8a 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -1,12 +1,13 @@ namespace OpenMcdf3; -public class MiniFatStream : Stream +internal class MiniFatStream : Stream { readonly IOContext ioContext; readonly MiniFatSectorChainEnumerator chain; readonly FatStream fatStream; - long length; + readonly long length; long position; + bool disposed; internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) { @@ -30,40 +31,63 @@ internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) public override long Position { get => position; - set => position = value; + set => Seek(value, SeekOrigin.Begin); } protected override void Dispose(bool disposing) { - chain.Dispose(); - fatStream.Dispose(); + if (!disposed) + { + chain.Dispose(); + fatStream.Dispose(); + disposed = true; + } base.Dispose(disposing); } public override void Flush() { + this.ThrowIfDisposed(disposed); fatStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number"); + + if ((uint)count > buffer.Length - offset) + 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"); + + this.ThrowIfDisposed(disposed); + + if (count == 0) + return 0; + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + if (maxCount == 0) + return 0; + int realCount = Math.Min(count, maxCount); int readCount = 0; do { MiniSector sector = chain.Current; int remaining = realCount - readCount; - long readLength = Math.Min(remaining, MiniSector.Length - sectorOffset); + long readLength = Math.Min(remaining, buffer.Length); fatStream.Position = sector.StartOffset + sectorOffset; - int read = fatStream.Read(buffer, offset + readCount, (int)readLength); + int localOffset = offset + readCount; + int read = fatStream.Read(buffer, localOffset, (int)readLength); if (read == 0) - return 0; + return readCount; position += read; readCount += read; sectorOffset = 0; @@ -76,14 +100,36 @@ public override int Read(byte[] buffer, int offset, int count) public override long Seek(long offset, SeekOrigin origin) { - position = offset; + this.ThrowIfDisposed(disposed); + + switch (origin) + { + case SeekOrigin.Begin: + if (offset < 0) + throw new IOException("Seek before origin"); + position = offset; + break; + + case SeekOrigin.Current: + if (position + offset < 0) + throw new IOException("Seek before origin"); + position += offset; + break; + + case SeekOrigin.End: + if (Length - offset < 0) + throw new IOException("Seek before origin"); + position = Length - offset; + break; + + default: + throw new ArgumentException(nameof(origin), "Invalid seek origin"); + } + return position; } - public override void SetLength(long value) - { - length = value; - } + public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 8ed3fe38..fd621eb0 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace OpenMcdf3; +namespace OpenMcdf3; public enum Version : ushort { @@ -10,18 +8,13 @@ public enum Version : ushort public sealed class RootStorage : Storage, IDisposable { - bool disposed; - public static RootStorage Create(string fileName, Version version = Version.V3) { FileStream stream = File.Create(fileName); Header header = new(version); McdfBinaryReader reader = new(stream); McdfBinaryWriter writer = new(stream); - IOContext ioContext = new(header, reader, writer) - { - RootEntry = new() - }; + IOContext ioContext = new(header, reader, writer, IOContextFlags.Create); return new RootStorage(ioContext); } @@ -42,8 +35,8 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) McdfBinaryReader reader = new(stream); McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); - IOContext ioContext = new(header, reader, writer, leaveOpen); - ioContext.RootEntry = ioContext.EnumerateDirectoryEntries().First(); + IOContextFlags contextFlags = leaveOpen ? IOContextFlags.LeaveOpen : IOContextFlags.None; + IOContext ioContext = new(header, reader, writer, contextFlags); return new RootStorage(ioContext); } @@ -52,12 +45,5 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) { } - public void Dispose() - { - if (disposed) - return; - - IOContext?.Dispose(); - disposed = true; - } + public void Dispose() => ioContext?.Dispose(); } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index b00bc73e..aea953db 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -2,25 +2,37 @@ public class Storage { - internal IOContext IOContext { get; } + internal readonly IOContext ioContext; internal DirectoryEntry DirectoryEntry { get; } internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) { - IOContext = ioContext; + this.ioContext = ioContext; DirectoryEntry = directoryEntry; } - public IEnumerable EnumerateEntries() => EnumerateDirectoryEntries() - .Select(e => e.ToEntryInfo()); + public IEnumerable EnumerateEntries() + { + this.ThrowIfDisposed(ioContext); - public IEnumerable EnumerateEntries(StorageType type) => EnumerateDirectoryEntries(type) - .Select(e => e.ToEntryInfo()); + return EnumerateDirectoryEntries() + .Select(e => e.ToEntryInfo()); + } + + public IEnumerable EnumerateEntries(StorageType type) + { + this.ThrowIfDisposed(ioContext); + + return EnumerateDirectoryEntries(type) + .Select(e => e.ToEntryInfo()); + } IEnumerable EnumerateDirectoryEntries() { - using DirectoryTreeEnumerator treeEnumerator = new(IOContext, DirectoryEntry); + this.ThrowIfDisposed(ioContext); + + using DirectoryTreeEnumerator treeEnumerator = new(ioContext, DirectoryEntry); while (treeEnumerator.MoveNext()) { yield return treeEnumerator.Current; @@ -32,18 +44,22 @@ IEnumerable EnumerateDirectoryEntries(StorageType type) => Enume public Storage OpenStorage(string name) { + this.ThrowIfDisposed(ioContext); + DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Storage) .FirstOrDefault(entry => entry.Name == name) ?? throw new DirectoryNotFoundException($"Directory not found {name}"); - return new Storage(IOContext, entry); + return new Storage(ioContext, entry); } public Stream OpenStream(string name) { + this.ThrowIfDisposed(ioContext); + DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); if (entry.StreamLength < Header.MiniStreamCutoffSize) - return new MiniFatStream(IOContext, entry); + return new MiniFatStream(ioContext, entry); else - return new FatStream(IOContext, entry); + return new FatStream(ioContext, entry); } } diff --git a/OpenMcdf3/ThrowHelper.cs b/OpenMcdf3/ThrowHelper.cs new file mode 100644 index 00000000..2ee7af58 --- /dev/null +++ b/OpenMcdf3/ThrowHelper.cs @@ -0,0 +1,16 @@ +namespace OpenMcdf3; + +internal static class ThrowHelper +{ + public static void ThrowIfDisposed(this object instance, bool disposed) + { + if (disposed) + throw new ObjectDisposedException(instance.GetType().FullName); + } + + public static void ThrowIfDisposed(this object instance, IOContext context) + { + if (context.IsDisposed) + throw new InvalidOperationException("Root storage has been disposed"); + } +} From fd8f692fd0ea6b0811a8606bc83337658d3c415b Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 16 Oct 2024 14:42:53 +1300 Subject: [PATCH 017/114] Add stream unit tests --- OpenMcdf3.Tests/BinaryReaderTests.cs | 6 ++- OpenMcdf3.Tests/CfbStreamTests.cs | 33 -------------- OpenMcdf3.Tests/EntryInfoTests.cs | 5 ++- OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 58 ++++++++++++++++++++++++- OpenMcdf3.Tests/StorageTests.cs | 7 +-- OpenMcdf3.Tests/StreamAssert.cs | 27 ++++++++++++ OpenMcdf3.Tests/StreamTests.cs | 44 +++++++++++++++++++ OpenMcdf3.Tests/TestStream_v3_0.cfs | Bin 0 -> 1536 bytes OpenMcdf3.Tests/TestStream_v3_4095.cfs | Bin 0 -> 6144 bytes OpenMcdf3.Tests/TestStream_v3_4096.cfs | Bin 0 -> 5632 bytes OpenMcdf3.Tests/TestStream_v3_4097.cfs | Bin 0 -> 6144 bytes OpenMcdf3.Tests/TestStream_v3_511.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_512.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_513.cfs | Bin 0 -> 3072 bytes OpenMcdf3.Tests/TestStream_v3_63.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_64.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_65.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_0.cfs | Bin 0 -> 1536 bytes OpenMcdf3.Tests/TestStream_v4_4095.cfs | Bin 0 -> 6144 bytes OpenMcdf3.Tests/TestStream_v4_4096.cfs | Bin 0 -> 5632 bytes OpenMcdf3.Tests/TestStream_v4_4097.cfs | Bin 0 -> 6144 bytes OpenMcdf3.Tests/TestStream_v4_511.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_512.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_513.cfs | Bin 0 -> 3072 bytes OpenMcdf3.Tests/TestStream_v4_63.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_64.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_65.cfs | Bin 0 -> 2560 bytes OpenMcdf3.Tests/_Test.ppt | Bin 174592 -> 0 bytes OpenMcdf3.Tests/test.cfb | Bin 1058304 -> 0 bytes 29 files changed, 135 insertions(+), 45 deletions(-) delete mode 100644 OpenMcdf3.Tests/CfbStreamTests.cs create mode 100644 OpenMcdf3.Tests/StreamAssert.cs create mode 100644 OpenMcdf3.Tests/StreamTests.cs create mode 100644 OpenMcdf3.Tests/TestStream_v3_0.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_4095.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_4096.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_4097.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_511.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_512.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_513.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_63.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_64.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v3_65.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_0.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_4095.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_4096.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_4097.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_511.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_512.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_513.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_63.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_64.cfs create mode 100644 OpenMcdf3.Tests/TestStream_v4_65.cfs delete mode 100644 OpenMcdf3.Tests/_Test.ppt delete mode 100644 OpenMcdf3.Tests/test.cfb diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf3.Tests/BinaryReaderTests.cs index 4e570624..46eff130 100644 --- a/OpenMcdf3.Tests/BinaryReaderTests.cs +++ b/OpenMcdf3.Tests/BinaryReaderTests.cs @@ -24,9 +24,11 @@ public void ReadFileTime() } [TestMethod] - public void ReadHeader() + [DataRow("TestStream_v3_0.cfs")] + [DataRow("TestStream_v4_0.cfs")] + public void ReadHeader(string fileName) { - using FileStream stream = File.OpenRead("_Test.ppt"); + using FileStream stream = File.OpenRead(fileName); using McdfBinaryReader reader = new(stream); Header header = reader.ReadHeader(); } diff --git a/OpenMcdf3.Tests/CfbStreamTests.cs b/OpenMcdf3.Tests/CfbStreamTests.cs deleted file mode 100644 index ff40b79f..00000000 --- a/OpenMcdf3.Tests/CfbStreamTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace OpenMcdf3.Tests; - -[TestClass] -public sealed class CfbStreamTests -{ - [TestMethod] - [DataRow("_Test.ppt", "Current User", 62)] - [DataRow("test.cfb", "MyStream0", 1048576)] - public void Read(string fileName, string streamName, long length) - { - using var rootStorage = RootStorage.OpenRead(fileName); - using Stream stream = rootStorage.OpenStream(streamName); - Assert.AreEqual(length, stream.Length); - - using MemoryStream memoryStream = new(); - stream.CopyTo(memoryStream); - Assert.AreEqual(length, memoryStream.Length); - } - - [TestMethod] - [DataRow("_Test.ppt")] - [DataRow("test.cfb")] - public void ReadAllStreams(string fileName) - { - using var rootStorage = RootStorage.OpenRead(fileName); - foreach (EntryInfo entryInfo in rootStorage.EnumerateEntries(StorageType.Stream)) - { - using Stream stream = rootStorage.OpenStream(entryInfo.Name); - using MemoryStream memoryStream = new(); - stream.CopyTo(memoryStream); - } - } -} diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf3.Tests/EntryInfoTests.cs index 290aec8e..60d6a320 100644 --- a/OpenMcdf3.Tests/EntryInfoTests.cs +++ b/OpenMcdf3.Tests/EntryInfoTests.cs @@ -4,8 +4,9 @@ public sealed class EntryInfoTests { [TestMethod] - [DataRow("_Test.ppt", 4)] - [DataRow("test.cfb", 1)] + [DataRow("MultipleStorage.cfs", 1)] + [DataRow("TestStream_v3_0.cfs", 1)] + [DataRow("TestStream_v4_0.cfs", 1)] public void EnumerateEntryInfos(string fileName, int count) { using var rootStorage = RootStorage.OpenRead(fileName); diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index bad7483e..0a325a95 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -38,10 +38,64 @@ PreserveNewest - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 68695a5c..67e8c9d2 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -1,12 +1,9 @@ -using System.Diagnostics; - -namespace OpenMcdf3.Tests; +namespace OpenMcdf3.Tests; [TestClass] public sealed class StorageTests { [TestMethod] - [DataRow("_Test.ppt", 0)] [DataRow("MultipleStorage.cfs", 1)] [DataRow("MultipleStorage2.cfs", 1)] [DataRow("MultipleStorage3.cfs", 1)] @@ -15,8 +12,6 @@ public void Read(string fileName, long storageCount) { using var rootStorage = RootStorage.OpenRead(fileName); IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); - //foreach (var entryInfo in storageEntries) - // Trace.WriteLine($"{entryInfo}"); Assert.AreEqual(storageCount, storageEntries.Count()); } } diff --git a/OpenMcdf3.Tests/StreamAssert.cs b/OpenMcdf3.Tests/StreamAssert.cs new file mode 100644 index 00000000..328cbe86 --- /dev/null +++ b/OpenMcdf3.Tests/StreamAssert.cs @@ -0,0 +1,27 @@ +namespace OpenMcdf3.Tests; + +internal static class StreamAssert +{ + public static void AreEqual(Stream expected, Stream actual, int bufferLength = 4096) + { + Assert.AreEqual(expected.Length, actual.Length); + + expected.Position = 0; + actual.Position = 0; + + byte[] expectedBuffer = new byte[bufferLength]; + byte[] actualBuffer = new byte[bufferLength]; + while (expected.Position < expected.Length) + { + int expectedRead = expected.Read(expectedBuffer, 0, expectedBuffer.Length); + int actualRead = actual.Read(actualBuffer, 0, actualBuffer.Length); + + if (expectedRead == bufferLength && actualRead == bufferLength) + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + else + CollectionAssert.AreEqual(expectedBuffer.Take(expectedRead).ToList(), actualBuffer.Take(actualRead).ToList()); + } + + Assert.AreEqual(expected.Position, actual.Position); + } +} diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs new file mode 100644 index 00000000..01d1e369 --- /dev/null +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -0,0 +1,44 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class StreamTests +{ + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] + [DataRow(Version.V4, 4097)] + public void Read(Version version, int length) + { + string fileName = $"TestStream_v{(int)version}_{length}.cfs"; + using var rootStorage = RootStorage.OpenRead(fileName); + using Stream stream = rootStorage.OpenStream("TestStream"); + Assert.AreEqual(length, stream.Length); + + // Test files are filled with bytes equal to their position modulo 256 + using MemoryStream expectedStream = new(length); + for (int i = 0; i < length; i++) + expectedStream.WriteByte((byte)i); + + using MemoryStream actualStream = new(); + stream.CopyTo(actualStream); + + StreamAssert.AreEqual(expectedStream, actualStream); + } +} diff --git a/OpenMcdf3.Tests/TestStream_v3_0.cfs b/OpenMcdf3.Tests/TestStream_v3_0.cfs new file mode 100644 index 0000000000000000000000000000000000000000..c3ee5ef6ddebb0ee8bec31dcf1ce2ac4d219681b GIT binary patch literal 1536 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3aa1_1`3{Qv(TAs7vk2MUdXArt~Z4EaD< z!l1z5%8&=7ix?`AW0@dZjDeLA=3SKZfk!7Sy?~U2Fr+dR1MLk4+nvgg$dHRiC#IAb s0~6eSkUKC{V2U8yk10#K2+aS)+E2O-_;pZXKPW8zA{#Ku9#J6x0JpaHj{pDw literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_4095.cfs b/OpenMcdf3.Tests/TestStream_v3_4095.cfs new file mode 100644 index 0000000000000000000000000000000000000000..effaf33da4b905593f2ce7e00c9f7b23284978bb GIT binary patch literal 6144 zcmeI$XHXMC7zW@?LV(ai2@rbky|;h`dj;%@q6RCVfCU>W_TGEL-W7Yr-W9NS1$*yZ z$@y-aIF2*U#F0Phewin?*_&i@w|jTHUrDjJG`~PvMt%wz5fB9@BAP$c^HA1NeKsOQ zgfgvDa2!XgwE;yIf6*BrJib6fsHvqb($Uq^H!w638=IJ#Nz5%QrB>E9ws!Uoj!w=l zu5Rugo?hNQzJC4zfkD9`p<&?>kx|hxve>vvm8(>ZuU5TA&04kV)U8*)LBoW^q(+UK zG;P+rMax#nt=puuZP#9&+M#2o&Rx2urDt^Oo|%>1qi3()Ieq%}>pviO;Gn^ILxv6; zK4Rpk(PPGr8$V&hhTfSoDs?}@Ou3KNQVdJLF zTefc7zGLUE-GzJh?%RLh;Gx4ujvhOH;^e8K;*!&6&YnAe;o_ysSFT>We&c3o*{$1m z?%uoq;NhdkPo6$|{^I4U*Kgjwd;j6%r_W!?6&yQ$*8gn$v+JMT|JePXJ^$GApUr>R z{Ey9l+5Dfaf7tr(KUn`N^EZ6tZ3LhJLZCSxEueXwa-ds^#lLq3X#Vb3{ZI3GG{;AC ze!8Fs`d|QtfD|ARgE5$ZDVTu-%)tUIK?+u24K`p4c3=+<;0R9O3@+dbZr~0c;0a#f z4L;xte&7!Q5C}mK3?UE-VGs@x5D8Hb4KW~tSYTtC;}S>)UfJl2c#@87Cdt8>Q9mR^ z$WvVvbS$YIbnqaOh@51h-9%;Ea*{&QzU@%+T|)TZ+o%6S&94!T4&Vw3)$#vM`xVr* J@@M{}{jZe^({KO) literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_4096.cfs b/OpenMcdf3.Tests/TestStream_v3_4096.cfs new file mode 100644 index 0000000000000000000000000000000000000000..068b6bc5510de9c42745c5afcb2f1e2dd691c225 GIT binary patch literal 5632 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxCyXz^0F?j#AH;>x96&ZuXcP>g5MW?r zVrF4wW9Q)H;^yJy;};MV5*85^6PJ*bl9rK`lUGnwQdUt_Q`gYc($>+{(>E|QGBzQN=`{lOV7y6 z%FfBn%P%M_DlRE4E3c@ms;;T6t8Zv*YHn$5Ywzgn>h9_7>z^=j(&Q;qr%j(RbJpxR zbLY)puyE1hB}NRWEt>3V5)8;K(w{73CbJy-Yd-v@>aPZLKBS()NKXLNZ z=`&}~oxgDL(&Z~xuU)@!^VaPo;%Tz5np>)8{W=zkUDl z^Vjb`fB*d-_5W!6kEZ|8{6AX$jh6qT_1|dyKid8oZU2q7e@EN@qy3-J{@+OG|ANMJ zn1GlWh*^M`6^KD&KI}jY8rS&?#2|VU49^exoIo~EXcP>g5MW?r zVrF4wW9Q)H;^yJy;};MV5*85^6PJ*bl9rK`lUGnwQdUt_Q`gYc($>+{(>E|QGBzQN=`{lOV7y6 z%FfBn%P%M_DlRE4E3c@ms;;T6t8Zv*YHn$5Ywzgn>h9_7>z^=j(&Q;qr%j(RbJpxR zbLY)puyE1hB}NRWEt>3V5)8;K(w{73CbJy-Yd-v@>aPZLKBS()NKXLNZ z=`&}~oxgDL(&Z~xuU)@!^VaPo;%Tz5np>)8{W=zkUDl z^Vjb`fB*d-_5W!6kEZ|8{6AX$jh6qT_1|dyKid8oZU2q7e@EN@qy3-J{@;k`|H8&| zM(Gh50-!NHCLm@8Viq7~1!B;cA3G3p05NE6?=KL8=ut5ILLi7C9|%hr6c}6?@_=*^ zLnScQi3ef~tchm7y4DZ!p;IRE9)`Tw;uXs}W;hg4+*r2V8;x XgAq7>O)UWPKPmQ8%WjZ4l-Lgd)osjj literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_511.cfs b/OpenMcdf3.Tests/TestStream_v3_511.cfs new file mode 100644 index 0000000000000000000000000000000000000000..7f51115edc26d39f1f4d41dc11fe9dea5332b8ec GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy@R8Zvx}>nyN9Qjw~w!%e?VYRa7buactm7WbWChqd_rPUa!P7i zdPZhec1~_yenDYTaY<=ec|~PabxmzueM4hYb4zPmdq-zicTaC$|AdK?CQq3EN?fQ+Iw{G9Ld++{(hmRgVdHU@6i<{9 literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_512.cfs b/OpenMcdf3.Tests/TestStream_v3_512.cfs new file mode 100644 index 0000000000000000000000000000000000000000..440560b20e37dc86d8af8ffdf89d1674b0dac0aa GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy@R8Zvx}>nyN9Qjw~w!%e?VYRa7buactm7WbWChqd_rPUa!P7i zdPZhec1~_yenDYTaY<=ec|~PabxmzueM4hYb4zPmdq-zicTaC$|AdK?CQq3EN?fQ+Iw{G9Ld++{(hmRgVdHU@6ifWQ72z%m$U?pmLoBh*^OcROYh-@o0J&{%IkI zAs+}!7!(*>8S;R15kn;~=7|Sl46KZ>Py(eTq5&w;0YL~uDnl{Q-e9oZsSJq>xey5= iNihZ{xcwk^5UCB4=!sU0Um?u@B-xMONRnhJu^#}v$IS`= literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_513.cfs b/OpenMcdf3.Tests/TestStream_v3_513.cfs new file mode 100644 index 0000000000000000000000000000000000000000..8c1f999a50206dd0836c92ae94b5fcbd3412ea57 GIT binary patch literal 3072 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3Bx3yd$o093)i022H6|NnoGFcT01nWJC` zg#ZI16Eh1d8#@Ol7dHqnoxOvjle3Gfo4bdnm$#3vpMOALP;f|SSa?KaRCG*iTzo=eQgTXa zT6#uiR(4KqUVcGgQE^FWS$RceRdr2mU427iQ*%peTYE=mS9ecuU;l)OlO|7@I&J!l znX_iknLBU(f`y9~FIl>5`HGdRRKIRx^4T8ox67L*}HH5frEz*A31vL z_=%IJPM`HPpYUcY(!?)`_4pFV&2 z`tAFVpTBSg=PuNuARZwC-0C+gXUjP6A literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_63.cfs b/OpenMcdf3.Tests/TestStream_v3_63.cfs new file mode 100644 index 0000000000000000000000000000000000000000..eb2839c0790d255b8640c8031ecf739b41e53bba GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-PEDB4HC5Do!Q-u(;2AR2^6!N>}M(KsJj@r~}=AclM(EMZV!aAn8? z(nSoFB$Z}j46KYWGePw~vA_YUFoYqMp%`dyFxc)?hD3&3VvPW+6Jub4+YfRFv6}6P URY-(7nEy$!p9o`#RzivW0G+q--v9sr literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_64.cfs b/OpenMcdf3.Tests/TestStream_v3_64.cfs new file mode 100644 index 0000000000000000000000000000000000000000..313d56d5c4b101302277e3a360fd33d3ebf934bd GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-QvJt*Es!O#r>P~QCu#2^}kN5RMnfzdc0S@DhT+aQK~AS_`}U~px~ z1JXqdl_V8rVhpT|Ff&2*Ke508sxX8hm7y4DZ!p;IRE9)`Tw;v?s}o~jg4+*r2eFzR Vh*e00I+*`Sv7ZQIiB>|1{Q!qt@*e;I literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v3_65.cfs b/OpenMcdf3.Tests/TestStream_v3_65.cfs new file mode 100644 index 0000000000000000000000000000000000000000..42c7962f567c7e71e13a75add032cb2b1744337b GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy#pxXN5PN{0Z`uk3&bEAgh#>13ISO8J+gc~>RVU{1To|TVF`l* zgDXQGkS=1VB&jSDV_;>3nFUHq#DWH>!Vrd3hGL+-!C<>n84?+Ci8TVOPK<#GZa>H! Z#AW_TGEL-W7Yr-W9NS1$*yZ z$@y-aIF2*U#F0Phewin?*_&i@w|jTHUrDjJG`~PvMt%wz5fB9@BAP$c^HA1NeKsOQ zgfgvDa2!XgwE;yIf6*BrJib6fsHvqb($Uq^H!w638=IJ#Nz5%QrB>E9ws!Uoj!w=l zu5Rugo?hNQzJC4zfkD9`p<&?>kx|hxve>vvm8(>ZuU5TA&04kV)U8*)LBoW^q(+UK zG;P+rMax#nt=puuZP#9&+M#2o&Rx2urDt^Oo|%>1qi3()Ieq%}>pviO;Gn^ILxv6; zK4Rpk(PPGr8$V&hhTfSoDs?}@Ou3KNQVdJLF zTefc7zGLUE-GzJh?%RLh;Gx4ujvhOH;^e8K;*!&6&YnAe;o_ysSFT>We&c3o*{$1m z?%uoq;NhdkPo6$|{^I4U*Kgjwd;j6%r_W!?6&yQ$*8gn$v+JMT|JePXJ^$GApUr>R z{Ey9l+5Dfaf7tr(KUn`N^EZ6tZ3LhJLZCSxEueXwa-ds^#lLq3X#Vb3{ZI3GG{;AC ze!8Fs`d|QtfD|ARgE5$ZDVTu-%)tUIK?+u24K`p4c3=+<;0R9O3@+dbZr~0c;0a#f z4L;xte&7!Q5C}mK3?UE-VGs@x5D8Hb4KW~tSYTtC;}S>)UfJl2c#@87Cdt8>Q9mR^ z$WvVvbS$YIbnqaOh@51h-9%;Ea*{&QzU@%+T|)TZ+o%6S&94!T4&Vw3)$#vM`xVr* J@@M{}{jZe^({KO) literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_4096.cfs b/OpenMcdf3.Tests/TestStream_v4_4096.cfs new file mode 100644 index 0000000000000000000000000000000000000000..068b6bc5510de9c42745c5afcb2f1e2dd691c225 GIT binary patch literal 5632 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxCyXz^0F?j#AH;>x96&ZuXcP>g5MW?r zVrF4wW9Q)H;^yJy;};MV5*85^6PJ*bl9rK`lUGnwQdUt_Q`gYc($>+{(>E|QGBzQN=`{lOV7y6 z%FfBn%P%M_DlRE4E3c@ms;;T6t8Zv*YHn$5Ywzgn>h9_7>z^=j(&Q;qr%j(RbJpxR zbLY)puyE1hB}NRWEt>3V5)8;K(w{73CbJy-Yd-v@>aPZLKBS()NKXLNZ z=`&}~oxgDL(&Z~xuU)@!^VaPo;%Tz5np>)8{W=zkUDl z^Vjb`fB*d-_5W!6kEZ|8{6AX$jh6qT_1|dyKid8oZU2q7e@EN@qy3-J{@+OG|ANMJ zn1GlWh*^M`6^KD&KI}jY8rS&?#2|VU49^exoIo~EXcP>g5MW?r zVrF4wW9Q)H;^yJy;};MV5*85^6PJ*bl9rK`lUGnwQdUt_Q`gYc($>+{(>E|QGBzQN=`{lOV7y6 z%FfBn%P%M_DlRE4E3c@ms;;T6t8Zv*YHn$5Ywzgn>h9_7>z^=j(&Q;qr%j(RbJpxR zbLY)puyE1hB}NRWEt>3V5)8;K(w{73CbJy-Yd-v@>aPZLKBS()NKXLNZ z=`&}~oxgDL(&Z~xuU)@!^VaPo;%Tz5np>)8{W=zkUDl z^Vjb`fB*d-_5W!6kEZ|8{6AX$jh6qT_1|dyKid8oZU2q7e@EN@qy3-J{@;k`|H8&| zM(Gh50-!NHCLm@8Viq7~1!B;cA3G3p05NE6?=KL8=ut5ILLi7C9|%hr6c}6?@_=*^ zLnScQi3ef~tchm7y4DZ!p;IRE9)`Tw;uXs}W;hg4+*r2V8;x XgAq7>O)UWPKPmQ8%WjZ4l-Lgd)osjj literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_511.cfs b/OpenMcdf3.Tests/TestStream_v4_511.cfs new file mode 100644 index 0000000000000000000000000000000000000000..7f51115edc26d39f1f4d41dc11fe9dea5332b8ec GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy@R8Zvx}>nyN9Qjw~w!%e?VYRa7buactm7WbWChqd_rPUa!P7i zdPZhec1~_yenDYTaY<=ec|~PabxmzueM4hYb4zPmdq-zicTaC$|AdK?CQq3EN?fQ+Iw{G9Ld++{(hmRgVdHU@6i<{9 literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_512.cfs b/OpenMcdf3.Tests/TestStream_v4_512.cfs new file mode 100644 index 0000000000000000000000000000000000000000..440560b20e37dc86d8af8ffdf89d1674b0dac0aa GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy@R8Zvx}>nyN9Qjw~w!%e?VYRa7buactm7WbWChqd_rPUa!P7i zdPZhec1~_yenDYTaY<=ec|~PabxmzueM4hYb4zPmdq-zicTaC$|AdK?CQq3EN?fQ+Iw{G9Ld++{(hmRgVdHU@6ifWQ72z%m$U?pmLoBh*^OcROYh-@o0J&{%IkI zAs+}!7!(*>8S;R15kn;~=7|Sl46KZ>Py(eTq5&w;0YL~uDnl{Q-e9oZsSJq>xey5= iNihZ{xcwk^5UCB4=!sU0Um?u@B-xMONRnhJu^#}v$IS`= literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_513.cfs b/OpenMcdf3.Tests/TestStream_v4_513.cfs new file mode 100644 index 0000000000000000000000000000000000000000..8c1f999a50206dd0836c92ae94b5fcbd3412ea57 GIT binary patch literal 3072 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3Bx3yd$o093)i022H6|NnoGFcT01nWJC` zg#ZI16Eh1d8#@Ol7dHqnoxOvjle3Gfo4bdnm$#3vpMOALP;f|SSa?KaRCG*iTzo=eQgTXa zT6#uiR(4KqUVcGgQE^FWS$RceRdr2mU427iQ*%peTYE=mS9ecuU;l)OlO|7@I&J!l znX_iknLBU(f`y9~FIl>5`HGdRRKIRx^4T8ox67L*}HH5frEz*A31vL z_=%IJPM`HPpYUcY(!?)`_4pFV&2 z`tAFVpTBSg=PuNuARZwC-0C+gXUjP6A literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_63.cfs b/OpenMcdf3.Tests/TestStream_v4_63.cfs new file mode 100644 index 0000000000000000000000000000000000000000..eb2839c0790d255b8640c8031ecf739b41e53bba GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-PEDB4HC5Do!Q-u(;2AR2^6!N>}M(KsJj@r~}=AclM(EMZV!aAn8? z(nSoFB$Z}j46KYWGePw~vA_YUFoYqMp%`dyFxc)?hD3&3VvPW+6Jub4+YfRFv6}6P URY-(7nEy$!p9o`#RzivW0G+q--v9sr literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_64.cfs b/OpenMcdf3.Tests/TestStream_v4_64.cfs new file mode 100644 index 0000000000000000000000000000000000000000..313d56d5c4b101302277e3a360fd33d3ebf934bd GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-QvJt*Es!O#r>P~QCu#2^}kN5RMnfzdc0S@DhT+aQK~AS_`}U~px~ z1JXqdl_V8rVhpT|Ff&2*Ke508sxX8hm7y4DZ!p;IRE9)`Tw;v?s}o~jg4+*r2eFzR Vh*e00I+*`Sv7ZQIiB>|1{Q!qt@*e;I literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_65.cfs b/OpenMcdf3.Tests/TestStream_v4_65.cfs new file mode 100644 index 0000000000000000000000000000000000000000..42c7962f567c7e71e13a75add032cb2b1744337b GIT binary patch literal 2560 zcmca`Uhu)fjZzO8(10BSGsD0CoD6J8;*3BxGmJ05z`z7#gT(&*|NkE(3}OSBqhJVy z00ScvGYcylI|nBhHxDl#zkr~Su!yLbxP+vXw2Z8ryn>>VvWlvjx`w8fwvMizzJZ~U zv5BdfxrL>bwT-Qvy#pxXN5PN{0Z`uk3&bEAgh#>13ISO8J+gc~>RVU{1To|TVF`l* zgDXQGkS=1VB&jSDV_;>3nFUHq#DWH>!Vrd3hGL+-!C<>n84?+Ci8TVOPK<#GZa>H! Z#AfN}x~@OHV8Z}vjoXgGLw#8! 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 sTd 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 diff --git a/OpenMcdf3.Tests/test.cfb b/OpenMcdf3.Tests/test.cfb deleted file mode 100644 index 861c57cbcc2b12a208cdab4262854f362298ca98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1058304 zcmeI&RSa|2p`g*fVPN8SCu<|Hn4>fPn}6{>P952K%@BN9_8DL;SBhMi|ie zAAkJo)_?2=H1?+&s0OY7}8m@+~5voJl2oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ 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 R_vZhvAOAml Date: Wed, 16 Oct 2024 16:30:31 +1300 Subject: [PATCH 018/114] Refactoring and bug fixes --- OpenMcdf3/DirectoryEntryEnumerator.cs | 2 +- OpenMcdf3/FatSectorChainEnumerator.cs | 2 +- OpenMcdf3/FatSectorEnumerator.cs | 20 ++++++++++++-------- OpenMcdf3/Header.cs | 6 +++--- OpenMcdf3/McdfBinaryReader.cs | 6 +++--- OpenMcdf3/McdfBinaryWriter.cs | 6 +++--- OpenMcdf3/MiniFatSectorEnumerator.cs | 8 ++++++-- OpenMcdf3/MiniSector.cs | 2 +- OpenMcdf3/Sector.cs | 2 +- OpenMcdf3/Storage.cs | 9 +++++---- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 655cbfd1..22982ea2 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -16,7 +16,7 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.ioContext = ioContext; this.version = (Version)ioContext.Header.MajorVersion; this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; - this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorID); + this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } public DirectoryEntry Current diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index 371c0016..b07493f2 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -72,8 +72,8 @@ public bool MoveTo(uint index) public void Reset() { - start = true; fatEnumerator.Reset(); + start = true; current = Sector.EndOfChain; Index = uint.MaxValue; } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 72258cf0..75a579ae 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -14,7 +14,7 @@ internal sealed class FatSectorEnumerator : IEnumerator public FatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - this.difatSectorId = ioContext.Header.FirstDifatSectorID; + this.difatSectorId = ioContext.Header.FirstDifatSectorId; } public Sector Current @@ -29,6 +29,10 @@ public Sector Current object IEnumerator.Current => Current; + public void Dispose() + { + } + public bool MoveNext() { if (start) @@ -89,14 +93,18 @@ public void Reset() { start = true; id = SectorType.EndOfChain; - difatSectorElementIndex = SectorType.EndOfChain; + difatSectorId = ioContext.Header.FirstDifatSectorId; + difatSectorElementIndex = 0; current = Sector.EndOfChain; } public uint GetNextFatSectorId(uint id) { - int elementLength = ioContext.Header.SectorSize / sizeof(uint); - uint sectorId = (uint)Math.DivRem(id, elementLength, out long sectorOffset); + if (id > SectorType.Maximum) + throw new ArgumentException("Invalid sector ID"); + + int elementCount = ioContext.Header.SectorSize / sizeof(uint); + uint sectorId = (uint)Math.DivRem(id, elementCount, out long sectorOffset); if (!MoveTo(sectorId)) throw new ArgumentException("Invalid sector ID"); @@ -105,8 +113,4 @@ public uint GetNextFatSectorId(uint id) uint nextId = ioContext.Reader.ReadUInt32(); return nextId; } - - public void Dispose() - { - } } diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 95e19d94..ef3b0ce0 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -47,18 +47,18 @@ public ushort SectorShift public uint FatSectorCount { get; set; } - public uint FirstDirectorySectorID { get; set; } = SectorType.EndOfChain; + public uint FirstDirectorySectorId { get; set; } = SectorType.EndOfChain; public uint TransactionSignature { get; set; } ///

/// This integer field contains the starting sector number for the mini FAT /// - public uint FirstMiniFatSectorID { get; set; } = SectorType.EndOfChain; + public uint FirstMiniFatSectorId { get; set; } = SectorType.EndOfChain; public uint MiniFatSectorCount { get; set; } - public uint FirstDifatSectorID { get; set; } = SectorType.EndOfChain; + public uint FirstDifatSectorId { get; set; } = SectorType.EndOfChain; public uint DifatSectorCount { get; set; } diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index 441445bf..a6ccfed4 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -40,14 +40,14 @@ public Header ReadHeader() this.FillBuffer(6); header.DirectorySectorCount = ReadUInt32(); header.FatSectorCount = ReadUInt32(); - header.FirstDirectorySectorID = ReadUInt32(); + header.FirstDirectorySectorId = ReadUInt32(); this.FillBuffer(4); uint miniStreamCutoffSize = ReadUInt32(); if (miniStreamCutoffSize != Header.MiniStreamCutoffSize) throw new FormatException("Mini stream cutoff size must be 4096 byte"); - header.FirstMiniFatSectorID = ReadUInt32(); + header.FirstMiniFatSectorId = ReadUInt32(); header.MiniFatSectorCount = ReadUInt32(); - header.FirstDifatSectorID = ReadUInt32(); + header.FirstDifatSectorId = ReadUInt32(); header.DifatSectorCount = ReadUInt32(); for (int i = 0; i < Header.DifatLength; i++) diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index 067036f1..6ebc343d 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -38,12 +38,12 @@ public void Write(Header header) WriteBytes(Header.Unused); Write(header.DirectorySectorCount); Write(header.FatSectorCount); - Write(header.FirstDirectorySectorID); + Write(header.FirstDirectorySectorId); Write((uint)0); Write(Header.MiniStreamCutoffSize); - Write(header.FirstMiniFatSectorID); + Write(header.FirstMiniFatSectorId); Write(header.MiniFatSectorCount); - Write(header.FirstDifatSectorID); + Write(header.FirstDifatSectorId); Write(header.DifatSectorCount); } diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index 9a516cbc..d2c17156 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -12,7 +12,7 @@ internal sealed class MiniFatSectorEnumerator : IEnumerator public MiniFatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - miniFatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorID); + miniFatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorId); } public MiniSector Current @@ -31,7 +31,7 @@ public bool MoveNext() { if (start) { - current = new(ioContext.Header.FirstMiniFatSectorID); + current = new(ioContext.Header.FirstMiniFatSectorId); start = false; } else if (!current.IsEndOfChain) @@ -59,11 +59,15 @@ public bool MoveTo(uint id) public void Reset() { + start = true; current = MiniSector.EndOfChain; } public uint GetNextMiniFatSectorId(uint id) { + if (id > SectorType.Maximum) + throw new ArgumentException("Invalid sector ID"); + int elementLength = ioContext.Header.SectorSize / sizeof(uint); uint sectorId = (uint)Math.DivRem(id, elementLength, out long sectorOffset); if (!miniFatChain.MoveTo(sectorId)) diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs index bf236065..0e5940f4 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf3/MiniSector.cs @@ -31,7 +31,7 @@ public readonly long EndOffset readonly void ThrowIfInvalid() { if (!IsValid) - throw new InvalidOperationException($"Invalid sector index: {Id}"); + throw new InvalidOperationException($"Invalid sector ID: {Id}"); } public override readonly string ToString() => $"{Id}"; diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 6a22ab8b..8aa10cff 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -33,7 +33,7 @@ public readonly long EndOffset readonly void ThrowIfInvalid() { if (!IsValid) - throw new InvalidOperationException($"Invalid sector index: {Id}"); + throw new InvalidOperationException($"Invalid sector ID: {Id}"); } public override readonly string ToString() => $"{Id}"; diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index aea953db..044407b5 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -57,9 +57,10 @@ public Stream OpenStream(string name) DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); - if (entry.StreamLength < Header.MiniStreamCutoffSize) - return new MiniFatStream(ioContext, entry); - else - return new FatStream(ioContext, entry); + return entry.StreamLength switch + { + < Header.MiniStreamCutoffSize => new MiniFatStream(ioContext, entry), + _ => new FatStream(ioContext, entry) + }; } } From 64f3b07a3637ae03270e6a2e9b2897211d0a2837 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 09:46:29 +1300 Subject: [PATCH 019/114] Refactor and add comments --- OpenMcdf3/DirectoryEntry.cs | 52 +++++++++++++++++++++++---- OpenMcdf3/DirectoryEntryEnumerator.cs | 9 +++-- OpenMcdf3/DirectoryTreeEnumerator.cs | 6 ++-- OpenMcdf3/FatSectorChainEnumerator.cs | 10 +++--- OpenMcdf3/FatSectorEnumerator.cs | 9 ++--- OpenMcdf3/FatStream.cs | 2 +- OpenMcdf3/McdfBinaryReader.cs | 10 +++--- OpenMcdf3/McdfBinaryWriter.cs | 8 ++--- OpenMcdf3/MiniFatStream.cs | 2 +- 9 files changed, 73 insertions(+), 35 deletions(-) diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index d28b07a2..47e95bfb 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// The storage type of a DirectoryEntry +/// public enum StorageType { Unallocated = 0, @@ -10,18 +13,27 @@ public enum StorageType Root = 5 } -enum Color +/// +/// Red-black node color +/// +enum NodeColor { Red = 0, Black = 1 } +/// +/// Stream ID constants for DirectoryEntry +/// internal static class StreamId { public const uint Maximum = 0xFFFFFFFA; public const uint NoStream = 0xFFFFFFFF; } +/// +/// Encapsulates data about a storage or stream +/// internal sealed class DirectoryEntry { internal const int Length = 128; @@ -51,18 +63,36 @@ public string Name public StorageType Type { get; set; } = StorageType.Unallocated; - public Color Color { get; set; } + public NodeColor Color { get; set; } - public uint LeftSiblingID { get; set; } + /// + /// Stream ID of the left sibling + /// + public uint LeftSiblingId { get; set; } - public uint RightSiblingID { get; set; } + /// + /// Stream ID of the right sibling + /// + public uint RightSiblingId { get; set; } - public uint ChildID { get; set; } + /// + /// Stream ID of the child + /// + public uint ChildId { get; set; } + /// + /// GUID for storage objects + /// public Guid CLSID { get; set; } + /// + /// User defined flags for storage objects + /// public uint StateBits { get; set; } + /// + /// The creation time of the storage object + /// public DateTime CreationTime { get => creationTime; @@ -75,6 +105,9 @@ public DateTime CreationTime } } + /// + /// The modified time of the storage object + /// public DateTime ModifiedTime { get => modifiedTime; @@ -87,9 +120,14 @@ public DateTime ModifiedTime } } - // Also the first sector of the mini-stream for the root storage object - public uint StartSectorLocation { get; set; } + /// + /// The starting sector location for a stream or the first sector of the mini-stream for the root storage object + /// + public uint StartSectorId { get; set; } + /// + /// The length of the stream + /// public long StreamLength { get; set; } public EntryInfo ToEntryInfo() => new() { Name = Name }; diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 22982ea2..848e38c8 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -18,6 +18,10 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } + public void Dispose() + { + chainEnumerator.Dispose(); + } public DirectoryEntry Current { @@ -68,9 +72,4 @@ public void Reset() entryIndex = -1; current = default!; } - - public void Dispose() - { - chainEnumerator.Dispose(); - } } diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index ab1aed59..45a1809a 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -12,7 +12,7 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); - child = directoryEntryEnumerator.Get(root.ChildID); + child = directoryEntryEnumerator.Get(root.ChildId); PushLeft(child); } @@ -42,7 +42,7 @@ public bool MoveNext() } current = stack.Pop(); - DirectoryEntry? rightSibling = directoryEntryEnumerator.Get(Current.RightSiblingID); + DirectoryEntry? rightSibling = directoryEntryEnumerator.Get(Current.RightSiblingId); PushLeft(rightSibling); return true; } @@ -59,7 +59,7 @@ private void PushLeft(DirectoryEntry? node) while (node is not null) { stack.Push(node); - node = directoryEntryEnumerator.Get(node.LeftSiblingID); + node = directoryEntryEnumerator.Get(node.LeftSiblingId); } } } diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index b07493f2..46c0841c 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -17,6 +17,11 @@ public FatSectorChainEnumerator(IOContext ioContext, uint startSectorId) fatEnumerator = new(ioContext); } + public void Dispose() + { + fatEnumerator.Dispose(); + } + public uint Index { get; private set; } = uint.MaxValue; public Sector Current @@ -77,9 +82,4 @@ public void Reset() current = Sector.EndOfChain; Index = uint.MaxValue; } - - public void Dispose() - { - fatEnumerator.Dispose(); - } } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 75a579ae..4853a46a 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -17,6 +17,11 @@ public FatSectorEnumerator(IOContext ioContext) this.difatSectorId = ioContext.Header.FirstDifatSectorId; } + public void Dispose() + { + // IOContext is owned by a parent + } + public Sector Current { get @@ -29,10 +34,6 @@ public Sector Current object IEnumerator.Current => Current; - public void Dispose() - { - } - public bool MoveNext() { if (start) diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index b47b4754..a9e66e17 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -13,7 +13,7 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) this.ioContext = ioContext; DirectoryEntry = directoryEntry; length = directoryEntry.StreamLength; - chain = new(ioContext, directoryEntry.StartSectorLocation); + chain = new(ioContext, directoryEntry.StartSectorId); } internal DirectoryEntry DirectoryEntry { get; private set; } diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index a6ccfed4..3c789304 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -60,7 +60,7 @@ public Header ReadHeader() public StorageType ReadStorageType() => (StorageType)ReadByte(); - public Color ReadColor() => (Color)ReadByte(); + public NodeColor ReadColor() => (NodeColor)ReadByte(); public DirectoryEntry ReadDirectoryEntry(Version version) { @@ -73,14 +73,14 @@ public DirectoryEntry ReadDirectoryEntry(Version version) entry.Name = Encoding.Unicode.GetString(buffer, 0, nameLength); entry.Type = ReadStorageType(); entry.Color = ReadColor(); - entry.LeftSiblingID = ReadUInt32(); - entry.RightSiblingID = ReadUInt32(); - entry.ChildID = ReadUInt32(); + entry.LeftSiblingId = ReadUInt32(); + entry.RightSiblingId = ReadUInt32(); + entry.ChildId = ReadUInt32(); entry.CLSID = ReadGuid(); entry.StateBits = ReadUInt32(); entry.CreationTime = ReadFileTime(); entry.ModifiedTime = ReadFileTime(); - entry.StartSectorLocation = ReadUInt32(); + entry.StartSectorId = ReadUInt32(); if (version == Version.V3) { diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index 6ebc343d..b3300ddc 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -54,14 +54,14 @@ public void Write(DirectoryEntry entry) Write(buffer, 0, DirectoryEntry.NameFieldLength); Write((byte)entry.Type); Write((byte)entry.Color); - Write(entry.LeftSiblingID); - Write(entry.RightSiblingID); - Write(entry.ChildID); + Write(entry.LeftSiblingId); + Write(entry.RightSiblingId); + Write(entry.ChildId); Write(entry.CLSID); Write(entry.StateBits); Write(entry.CreationTime); Write(entry.ModifiedTime); - Write(entry.StartSectorLocation); + Write(entry.StartSectorId); Write(entry.StreamLength); } } diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index ce081f8a..af531d1f 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -14,7 +14,7 @@ internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) this.ioContext = ioContext; DirectoryEntry = directoryEntry; length = directoryEntry.StreamLength; - chain = new(ioContext, directoryEntry.StartSectorLocation); + chain = new(ioContext, directoryEntry.StartSectorId); fatStream = new(ioContext, ioContext.RootEntry); } From 4c55bd410faf0f772419b41d976ba06659dbc299 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 09:49:03 +1300 Subject: [PATCH 020/114] Improve header validation --- OpenMcdf3/Header.cs | 1 + OpenMcdf3/McdfBinaryReader.cs | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index ef3b0ce0..57585061 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -4,6 +4,7 @@ internal sealed class Header { internal const int DifatLength = 109; + internal const ushort ExpectedMinorVersion = 0x003E; internal const ushort LittleEndian = 0xFFFE; internal const ushort SectorShiftV3 = 0x0009; internal const ushort SectorShiftV4 = 0x000C; diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index 3c789304..cfe47da0 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -26,10 +26,16 @@ public Header ReadHeader() Header header = new(); Read(buffer, 0, Header.Signature.Length); if (!buffer.Take(Header.Signature.Length).SequenceEqual(Header.Signature)) - throw new FormatException("Invalid signature"); + throw new FormatException("Invalid header signature"); header.CLSID = ReadGuid(); + if (header.CLSID != Guid.Empty) + throw new FormatException($"Invalid header CLSID: {header.CLSID}"); header.MinorVersion = ReadUInt16(); header.MajorVersion = ReadUInt16(); + if (header.MajorVersion is not (ushort)Version.V3 and not (ushort)Version.V4) + throw new FormatException($"Unsupported major version: {header.MajorVersion}"); + else if (header.MinorVersion is not Header.ExpectedMinorVersion) + throw new FormatException($"Unsupported minor version: {header.MinorVersion}"); ushort byteOrder = ReadUInt16(); if (byteOrder != Header.LittleEndian) throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})"); @@ -58,9 +64,21 @@ public Header ReadHeader() return header; } - public StorageType ReadStorageType() => (StorageType)ReadByte(); + public StorageType ReadStorageType() + { + var type = (StorageType)ReadByte(); + if (type is not StorageType.Storage and not StorageType.Stream and not StorageType.Root and not StorageType.Unallocated) + throw new FormatException($"Invalid storage type: {type}"); + return type; + } - public NodeColor ReadColor() => (NodeColor)ReadByte(); + public NodeColor ReadColor() + { + var color = (NodeColor)ReadByte(); + if (color is not NodeColor.Black and not NodeColor.Red) + throw new FormatException($"Invalid node color: {color}"); + return color; + } public DirectoryEntry ReadDirectoryEntry(Version version) { From 8ebe41a0a54a7df07b270a11f406363865a28754 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 10:09:39 +1300 Subject: [PATCH 021/114] Additional comments --- OpenMcdf3/DirectoryEntry.cs | 29 +++++++++++++++------------ OpenMcdf3/DirectoryEntryEnumerator.cs | 13 +++++++++--- OpenMcdf3/DirectoryTreeEnumerator.cs | 9 ++++++--- OpenMcdf3/EntryInfo.cs | 3 +++ OpenMcdf3/FatSectorChainEnumerator.cs | 13 ++++++++++++ OpenMcdf3/FatSectorEnumerator.cs | 3 +++ OpenMcdf3/FatStream.cs | 3 +++ OpenMcdf3/Header.cs | 10 ++++++++- 8 files changed, 63 insertions(+), 20 deletions(-) diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 47e95bfb..2159e8a6 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -3,7 +3,7 @@ namespace OpenMcdf3; /// -/// The storage type of a DirectoryEntry +/// The storage type of a . /// public enum StorageType { @@ -14,7 +14,7 @@ public enum StorageType } /// -/// Red-black node color +/// Red-black node color. /// enum NodeColor { @@ -23,7 +23,7 @@ enum NodeColor } /// -/// Stream ID constants for DirectoryEntry +/// Stream ID constants for . /// internal static class StreamId { @@ -32,7 +32,7 @@ internal static class StreamId } /// -/// Encapsulates data about a storage or stream +/// Encapsulates data about a or Stream. /// internal sealed class DirectoryEntry { @@ -61,37 +61,40 @@ public string Name } } + /// + /// The type of the storage object. + /// public StorageType Type { get; set; } = StorageType.Unallocated; public NodeColor Color { get; set; } /// - /// Stream ID of the left sibling + /// Stream ID of the left sibling. /// public uint LeftSiblingId { get; set; } /// - /// Stream ID of the right sibling + /// Stream ID of the right sibling. /// public uint RightSiblingId { get; set; } /// - /// Stream ID of the child + /// Stream ID of the child. /// public uint ChildId { get; set; } /// - /// GUID for storage objects + /// GUID for storage objects. /// public Guid CLSID { get; set; } /// - /// User defined flags for storage objects + /// User defined flags for storage objects. /// public uint StateBits { get; set; } /// - /// The creation time of the storage object + /// The creation time of the storage object. /// public DateTime CreationTime { @@ -106,7 +109,7 @@ public DateTime CreationTime } /// - /// The modified time of the storage object + /// The modified time of the storage object. /// public DateTime ModifiedTime { @@ -121,12 +124,12 @@ public DateTime ModifiedTime } /// - /// The starting sector location for a stream or the first sector of the mini-stream for the root storage object + /// The starting sector location for a stream or the first sector of the mini-stream for the root storage object. /// public uint StartSectorId { get; set; } /// - /// The length of the stream + /// The length of the stream. /// public long StreamLength { get; set; } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 848e38c8..c4e26bcc 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates instances from a . +/// internal sealed class DirectoryEntryEnumerator : IEnumerator { private readonly IOContext ioContext; @@ -18,6 +21,7 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } + public void Dispose() { chainEnumerator.Dispose(); @@ -51,12 +55,15 @@ public bool MoveNext() return current.Type != StorageType.Unallocated; } - public DirectoryEntry? Get(uint id) + /// + /// Gets the for the specified stream ID. + /// + public DirectoryEntry? GetDictionaryEntry(uint streamId) { - if (id == StreamId.NoStream) + if (streamId == StreamId.NoStream) return null; - uint chainIndex = (uint)Math.DivRem(id, entryCount, out long entryIndex); + uint chainIndex = (uint)Math.DivRem(streamId, entryCount, out long entryIndex); if (!chainEnumerator.MoveTo(chainIndex)) throw new ArgumentException("Invalid directory entry ID"); diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 45a1809a..cf21a605 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates the children of a . +/// internal sealed class DirectoryTreeEnumerator : IEnumerator { private readonly DirectoryEntry? child; @@ -12,7 +15,7 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); - child = directoryEntryEnumerator.Get(root.ChildId); + child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); PushLeft(child); } @@ -42,7 +45,7 @@ public bool MoveNext() } current = stack.Pop(); - DirectoryEntry? rightSibling = directoryEntryEnumerator.Get(Current.RightSiblingId); + DirectoryEntry? rightSibling = directoryEntryEnumerator.GetDictionaryEntry(Current.RightSiblingId); PushLeft(rightSibling); return true; } @@ -59,7 +62,7 @@ private void PushLeft(DirectoryEntry? node) while (node is not null) { stack.Push(node); - node = directoryEntryEnumerator.Get(node.LeftSiblingId); + node = directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); } } } diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs index c22a137b..b457ad3b 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf3/EntryInfo.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// Encapsulates information about an entry in a . +/// public class EntryInfo { public string Name { get; internal set; } = string.Empty; diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index 46c0841c..4a214399 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates the s in a FAT sector chain. +/// internal sealed class FatSectorChainEnumerator : IEnumerator { private readonly IOContext ioContext; @@ -22,6 +25,9 @@ public void Dispose() fatEnumerator.Dispose(); } + /// + /// The index within the FAT sector chain, or if the enumeration has not started. + /// public uint Index { get; private set; } = uint.MaxValue; public Sector Current @@ -36,6 +42,7 @@ public Sector Current object IEnumerator.Current => Current; + /// public bool MoveNext() { if (start) @@ -61,6 +68,11 @@ public bool MoveNext() return true; } + /// + /// Moves to the specified index within the FAT sector chain. + /// + /// + /// true if the enumerator was successfully advanced to the given index public bool MoveTo(uint index) { if (index < Index) @@ -75,6 +87,7 @@ public bool MoveTo(uint index) return true; } + /// public void Reset() { fatEnumerator.Reset(); diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 4853a46a..e5701be5 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates the FAT sectors of a compound file. +/// internal sealed class FatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index a9e66e17..44159c1d 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// Provides a for a stream object in a compound file./> +/// internal class FatStream : Stream { readonly IOContext ioContext; diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 57585061..1bcbbf64 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -1,6 +1,8 @@ namespace OpenMcdf3; - +/// +/// The structure at the beginning of a compound file +/// internal sealed class Header { internal const int DifatLength = 109; @@ -11,12 +13,18 @@ internal sealed class Header internal const short MiniSectorShift = 6; internal const uint MiniStreamCutoffSize = 4096; + /// + /// Identification signature for the compound file structure + /// internal static readonly byte[] Signature = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; internal static readonly byte[] Unused = new byte[6]; private ushort majorVersion; private ushort sectorShift = SectorShiftV3; + /// + /// Reserved and unused class ID + /// public Guid CLSID { get; set; } public ushort MinorVersion { get; set; } From 4d8600116d496135dbd76de1ccfb256b94b0e5e7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 10:16:09 +1300 Subject: [PATCH 022/114] Seal all the things --- OpenMcdf3/McdfBinaryReader.cs | 2 +- OpenMcdf3/McdfBinaryWriter.cs | 2 +- OpenMcdf3/MiniFatStream.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/McdfBinaryReader.cs index cfe47da0..85549c6f 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/McdfBinaryReader.cs @@ -2,7 +2,7 @@ namespace OpenMcdf3; -internal class McdfBinaryReader : BinaryReader +internal sealed class McdfBinaryReader : BinaryReader { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/McdfBinaryWriter.cs index b3300ddc..d456f590 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/McdfBinaryWriter.cs @@ -2,7 +2,7 @@ namespace OpenMcdf3; -internal class McdfBinaryWriter : BinaryWriter +internal sealed class McdfBinaryWriter : BinaryWriter { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index af531d1f..9854a907 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -1,6 +1,6 @@ namespace OpenMcdf3; -internal class MiniFatStream : Stream +internal sealed class MiniFatStream : Stream { readonly IOContext ioContext; readonly MiniFatSectorChainEnumerator chain; From 1a47cefbbd3167f1bb5783b6b349083cfe6e3a92 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 10:35:49 +1300 Subject: [PATCH 023/114] Add comments and refactor --- OpenMcdf3.Tests/BinaryReaderTests.cs | 6 +-- ...McdfBinaryReader.cs => CfbBinaryReader.cs} | 9 ++-- ...McdfBinaryWriter.cs => CfbBinaryWriter.cs} | 7 ++- OpenMcdf3/DirectoryEntryEnumerator.cs | 5 ++ OpenMcdf3/DirectoryTreeEnumerator.cs | 5 ++ OpenMcdf3/FatSectorChainEnumerator.cs | 3 ++ OpenMcdf3/FatSectorEnumerator.cs | 9 +++- OpenMcdf3/FatStream.cs | 12 +++++ OpenMcdf3/Header.cs | 49 ++++++++++++++++--- OpenMcdf3/IOContext.cs | 12 +++-- OpenMcdf3/MiniFatSectorChainEnumerator.cs | 16 ++++++ OpenMcdf3/MiniFatSectorEnumerator.cs | 7 +++ OpenMcdf3/MiniFatStream.cs | 3 ++ OpenMcdf3/MiniSector.cs | 4 ++ OpenMcdf3/RootStorage.cs | 11 +++-- OpenMcdf3/Sector.cs | 5 ++ OpenMcdf3/SectorType.cs | 3 ++ OpenMcdf3/Storage.cs | 3 ++ OpenMcdf3/ThrowHelper.cs | 3 ++ 19 files changed, 150 insertions(+), 22 deletions(-) rename OpenMcdf3/{McdfBinaryReader.cs => CfbBinaryReader.cs} (95%) rename OpenMcdf3/{McdfBinaryWriter.cs => CfbBinaryWriter.cs} (91%) diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf3.Tests/BinaryReaderTests.cs index 46eff130..e5bfa095 100644 --- a/OpenMcdf3.Tests/BinaryReaderTests.cs +++ b/OpenMcdf3.Tests/BinaryReaderTests.cs @@ -8,7 +8,7 @@ public void ReadGuid() { byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; using MemoryStream stream = new(bytes); - using McdfBinaryReader reader = new(stream); + using CfbBinaryReader reader = new(stream); Guid guid = reader.ReadGuid(); Assert.AreEqual(new Guid(bytes), guid); } @@ -18,7 +18,7 @@ public void ReadFileTime() { byte[] bytes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; using MemoryStream stream = new(bytes); - using McdfBinaryReader reader = new(stream); + using CfbBinaryReader reader = new(stream); DateTime actual = reader.ReadFileTime(); Assert.AreEqual(DirectoryEntry.ZeroFileTime, actual); } @@ -29,7 +29,7 @@ public void ReadFileTime() public void ReadHeader(string fileName) { using FileStream stream = File.OpenRead(fileName); - using McdfBinaryReader reader = new(stream); + using CfbBinaryReader reader = new(stream); Header header = reader.ReadHeader(); } } diff --git a/OpenMcdf3/McdfBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs similarity index 95% rename from OpenMcdf3/McdfBinaryReader.cs rename to OpenMcdf3/CfbBinaryReader.cs index 85549c6f..7ddb13b2 100644 --- a/OpenMcdf3/McdfBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -2,11 +2,14 @@ namespace OpenMcdf3; -internal sealed class McdfBinaryReader : BinaryReader +/// +/// Reads CFB data types from a stream. +/// +internal sealed class CfbBinaryReader : BinaryReader { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; - public McdfBinaryReader(Stream input) + public CfbBinaryReader(Stream input) : base(input, Encoding.Unicode, true) { } @@ -56,7 +59,7 @@ public Header ReadHeader() header.FirstDifatSectorId = ReadUInt32(); header.DifatSectorCount = ReadUInt32(); - for (int i = 0; i < Header.DifatLength; i++) + for (int i = 0; i < Header.DifatArrayLength; i++) { header.Difat[i] = ReadUInt32(); } diff --git a/OpenMcdf3/McdfBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs similarity index 91% rename from OpenMcdf3/McdfBinaryWriter.cs rename to OpenMcdf3/CfbBinaryWriter.cs index d456f590..f8f08e0a 100644 --- a/OpenMcdf3/McdfBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -2,11 +2,14 @@ namespace OpenMcdf3; -internal sealed class McdfBinaryWriter : BinaryWriter +/// +/// Writes CFB data types to a stream. +/// +internal sealed class CfbBinaryWriter : BinaryWriter { readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; - public McdfBinaryWriter(Stream input) + public CfbBinaryWriter(Stream input) : base(input, Encoding.Unicode, true) { } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index c4e26bcc..9fb62f42 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -22,11 +22,13 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } + /// public void Dispose() { chainEnumerator.Dispose(); } + /// public DirectoryEntry Current { get @@ -37,8 +39,10 @@ public DirectoryEntry Current } } + /// object IEnumerator.Current => Current; + /// public bool MoveNext() { if (entryIndex == -1 || entryIndex >= entryCount) @@ -73,6 +77,7 @@ public bool MoveNext() return current; } + /// public void Reset() { chainEnumerator.Reset(); diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index cf21a605..42d786ad 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -19,11 +19,13 @@ internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) PushLeft(child); } + /// public void Dispose() { directoryEntryEnumerator.Dispose(); } + /// public DirectoryEntry Current { get @@ -34,8 +36,10 @@ public DirectoryEntry Current } } + /// object IEnumerator.Current => Current; + /// public bool MoveNext() { if (stack.Count == 0) @@ -50,6 +54,7 @@ public bool MoveNext() return true; } + /// public void Reset() { current = null; diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index 4a214399..f06757a1 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -20,6 +20,7 @@ public FatSectorChainEnumerator(IOContext ioContext, uint startSectorId) fatEnumerator = new(ioContext); } + /// public void Dispose() { fatEnumerator.Dispose(); @@ -30,6 +31,7 @@ public void Dispose() ///
public uint Index { get; private set; } = uint.MaxValue; + /// public Sector Current { get @@ -40,6 +42,7 @@ public Sector Current } } + /// object IEnumerator.Current => Current; /// diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index e5701be5..03cd07bb 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -20,11 +20,13 @@ public FatSectorEnumerator(IOContext ioContext) this.difatSectorId = ioContext.Header.FirstDifatSectorId; } + /// public void Dispose() { // IOContext is owned by a parent } + /// public Sector Current { get @@ -35,8 +37,10 @@ public Sector Current } } + /// object IEnumerator.Current => Current; + /// public bool MoveNext() { if (start) @@ -47,7 +51,7 @@ public bool MoveNext() id++; - if (id < ioContext.Header.FatSectorCount && id < Header.DifatLength) + if (id < ioContext.Header.FatSectorCount && id < Header.DifatArrayLength) { uint id = ioContext.Header.Difat[this.id]; current = new Sector(id, ioContext.Header.SectorSize); @@ -79,6 +83,7 @@ public bool MoveNext() return true; } + /// public bool MoveTo(uint sectorId) { if (sectorId < id) @@ -93,6 +98,7 @@ public bool MoveTo(uint sectorId) return true; } + /// public void Reset() { start = true; @@ -102,6 +108,7 @@ public void Reset() current = Sector.EndOfChain; } + /// public uint GetNextFatSectorId(uint id) { if (id > SectorType.Maximum) diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 44159c1d..eb526f73 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -19,22 +19,29 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) chain = new(ioContext, directoryEntry.StartSectorId); } + /// internal DirectoryEntry DirectoryEntry { get; private set; } + /// public override bool CanRead => true; + /// public override bool CanSeek => true; + /// public override bool CanWrite => false; + /// public override long Length => length; + /// public override long Position { get => position; set => Seek(value, SeekOrigin.Begin); } + /// protected override void Dispose(bool disposing) { if (!disposed) @@ -46,8 +53,10 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// public override void Flush() => this.ThrowIfDisposed(disposed); + /// public override int Read(byte[] buffer, int offset, int count) { if (buffer is null) @@ -94,6 +103,7 @@ public override int Read(byte[] buffer, int offset, int count) return readCount; } + /// public override long Seek(long offset, SeekOrigin origin) { this.ThrowIfDisposed(disposed); @@ -125,7 +135,9 @@ public override long Seek(long offset, SeekOrigin origin) return position; } + /// public override void SetLength(long value) => throw new NotSupportedException(); + /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 1bcbbf64..93febf4f 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -1,11 +1,11 @@ namespace OpenMcdf3; /// -/// The structure at the beginning of a compound file +/// The structure at the beginning of a compound file. /// internal sealed class Header { - internal const int DifatLength = 109; + internal const int DifatArrayLength = 109; internal const ushort ExpectedMinorVersion = 0x003E; internal const ushort LittleEndian = 0xFFFE; internal const ushort SectorShiftV3 = 0x0009; @@ -14,21 +14,28 @@ internal sealed class Header internal const uint MiniStreamCutoffSize = 4096; /// - /// Identification signature for the compound file structure + /// Identification signature for the compound file structure. /// internal static readonly byte[] Signature = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; internal static readonly byte[] Unused = new byte[6]; + private ushort majorVersion; private ushort sectorShift = SectorShiftV3; /// - /// Reserved and unused class ID + /// Reserved and unused class ID. /// public Guid CLSID { get; set; } + /// + /// Version number for non-breaking changes. + /// public ushort MinorVersion { get; set; } + /// + /// Version number for breaking changes. + /// public ushort MajorVersion { get => majorVersion; set @@ -39,6 +46,9 @@ public ushort MajorVersion } } + /// + /// Specifies the sector size of the compound file. + /// public ushort SectorShift { get => sectorShift; set @@ -52,27 +62,54 @@ public ushort SectorShift } } + /// + /// The number of directory sectors in the compound file. + /// public uint DirectorySectorCount { get; set; } + /// + /// The number of FAT sectors in the compound file. + /// public uint FatSectorCount { get; set; } + /// + /// The starting sector ID of the directory stream. + /// public uint FirstDirectorySectorId { get; set; } = SectorType.EndOfChain; + /// + /// A sequence number that is incremented every time the compound file is saved by an implementation that supports file transactions. + /// public uint TransactionSignature { get; set; } /// - /// This integer field contains the starting sector number for the mini FAT + /// This integer field contains the starting sector ID of the mini FAT. /// public uint FirstMiniFatSectorId { get; set; } = SectorType.EndOfChain; + /// + /// The number of sectors in the mini FAT. + /// public uint MiniFatSectorCount { get; set; } + /// + /// The starting sector ID of the DIFAT. + /// public uint FirstDifatSectorId { get; set; } = SectorType.EndOfChain; + /// + /// The number of DIFACT sectors in the compound file. + /// public uint DifatSectorCount { get; set; } - public uint[] Difat { get; } = new uint[DifatLength]; + /// + /// An array of the first FAT sector IDs. + /// + public uint[] Difat { get; } = new uint[DifatArrayLength]; + /// + /// The size of a regular sector. + /// public int SectorSize => 1 << SectorShift; public Header(Version version = Version.V3) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index e70f4f15..44b8de09 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -7,19 +7,22 @@ enum IOContextFlags LeaveOpen = 2 } +/// +/// Encapsulates the objects required to read and write data to and from a compound file. +/// internal sealed class IOContext : IDisposable { public Header Header { get; } - public McdfBinaryReader Reader { get; } + public CfbBinaryReader Reader { get; } - public McdfBinaryWriter? Writer { get; } + public CfbBinaryWriter? Writer { get; } public DirectoryEntry RootEntry { get; } public bool IsDisposed { get; private set; } - public IOContext(Header header, McdfBinaryReader reader, McdfBinaryWriter? writer, IOContextFlags contextFlags = IOContextFlags.None) + public IOContext(Header header, CfbBinaryReader reader, CfbBinaryWriter? writer, IOContextFlags contextFlags = IOContextFlags.None) { Header = header; Reader = reader; @@ -39,6 +42,9 @@ public void Dispose() } } + /// + /// Enumerates all the instances in the compound file. + /// public IEnumerable EnumerateDirectoryEntries() { this.ThrowIfDisposed(IsDisposed); diff --git a/OpenMcdf3/MiniFatSectorChainEnumerator.cs b/OpenMcdf3/MiniFatSectorChainEnumerator.cs index b1296c16..dc838d7d 100644 --- a/OpenMcdf3/MiniFatSectorChainEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorChainEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates the s in a Mini FAT sector chain. +/// internal sealed class MiniFatSectorChainEnumerator : IEnumerator { private readonly MiniFatSectorEnumerator miniFatEnumerator; @@ -15,8 +18,12 @@ public MiniFatSectorChainEnumerator(IOContext ioContext, uint startSectorId) miniFatEnumerator = new(ioContext); } + /// + /// The index within the Mini FAT sector chain, or if the enumeration has not started. + /// public uint Index { get; private set; } = uint.MaxValue; + /// public MiniSector Current { get @@ -27,8 +34,10 @@ public MiniSector Current } } + /// object IEnumerator.Current => Current; + /// public bool MoveNext() { if (start) @@ -54,6 +63,11 @@ public bool MoveNext() return true; } + /// + /// Moves to the specified index within the mini FAT sector chain. + /// + /// + /// true if the enumerator was successfully advanced to the given index public bool MoveTo(uint index) { if (index < Index) @@ -68,6 +82,7 @@ public bool MoveTo(uint index) return true; } + /// public void Reset() { start = true; @@ -76,6 +91,7 @@ public void Reset() Index = uint.MaxValue; } + /// public void Dispose() { miniFatEnumerator.Dispose(); diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index d2c17156..277e9a38 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf3; +/// +/// Enumerates the s in a FAT sector chain. +/// internal sealed class MiniFatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; @@ -15,6 +18,7 @@ public MiniFatSectorEnumerator(IOContext ioContext) miniFatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorId); } + /// public MiniSector Current { get @@ -25,8 +29,10 @@ public MiniSector Current } } + /// object IEnumerator.Current => Current; + /// public bool MoveNext() { if (start) @@ -57,6 +63,7 @@ public bool MoveTo(uint id) return true; } + /// public void Reset() { start = true; diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index 9854a907..b7df0ac6 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// Provides a for reading a from the mini FAT stream. +/// internal sealed class MiniFatStream : Stream { readonly IOContext ioContext; diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs index 0e5940f4..38331d93 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf3/MiniSector.cs @@ -1,5 +1,9 @@ namespace OpenMcdf3; +/// +/// Encapsulates information about a mini sector in a compound file. +/// +/// The ID of the mini sector internal record struct MiniSector(uint Id) { public const int Length = 64; diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index fd621eb0..3df2d7ff 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -6,14 +6,17 @@ public enum Version : ushort V4 = 4 } +/// +/// Encapsulates the root of a compound file. +/// public sealed class RootStorage : Storage, IDisposable { public static RootStorage Create(string fileName, Version version = Version.V3) { FileStream stream = File.Create(fileName); Header header = new(version); - McdfBinaryReader reader = new(stream); - McdfBinaryWriter writer = new(stream); + CfbBinaryReader reader = new(stream); + CfbBinaryWriter writer = new(stream); IOContext ioContext = new(header, reader, writer, IOContextFlags.Create); return new RootStorage(ioContext); } @@ -32,8 +35,8 @@ public static RootStorage OpenRead(string fileName) public static RootStorage Open(Stream stream, bool leaveOpen = false) { - McdfBinaryReader reader = new(stream); - McdfBinaryWriter? writer = stream.CanWrite ? new(stream) : null; + CfbBinaryReader reader = new(stream); + CfbBinaryWriter? writer = stream.CanWrite ? new(stream) : null; Header header = reader.ReadHeader(); IOContextFlags contextFlags = leaveOpen ? IOContextFlags.LeaveOpen : IOContextFlags.None; IOContext ioContext = new(header, reader, writer, contextFlags); diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 8aa10cff..f3f64af1 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -1,5 +1,10 @@ namespace OpenMcdf3; +/// +/// Encapsulates information about a sector in a compound file. +/// +/// The sector ID +/// The sector length internal record struct Sector(uint Id, int Length) { public static readonly Sector EndOfChain = new(SectorType.EndOfChain, 0); diff --git a/OpenMcdf3/SectorType.cs b/OpenMcdf3/SectorType.cs index f4820042..83098373 100644 --- a/OpenMcdf3/SectorType.cs +++ b/OpenMcdf3/SectorType.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// Defines the types of sectors in a compound file. +/// internal static class SectorType { public const uint Maximum = 0xFFFFFFFA; diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 044407b5..444ddbdc 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// An object in a compound file that is analogous to a file system directory. +/// public class Storage { internal readonly IOContext ioContext; diff --git a/OpenMcdf3/ThrowHelper.cs b/OpenMcdf3/ThrowHelper.cs index 2ee7af58..aed8877a 100644 --- a/OpenMcdf3/ThrowHelper.cs +++ b/OpenMcdf3/ThrowHelper.cs @@ -1,5 +1,8 @@ namespace OpenMcdf3; +/// +/// Extensions to consistently throw exceptions. +/// internal static class ThrowHelper { public static void ThrowIfDisposed(this object instance, bool disposed) From 344120558f02af1cac773f57156eda28928c3bb6 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 10:55:32 +1300 Subject: [PATCH 024/114] Improve DirectoryEntryEnumerator validation --- OpenMcdf3/DirectoryEntryEnumerator.cs | 14 +++++++++----- OpenMcdf3/DirectoryTreeEnumerator.cs | 13 +++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 9fb62f42..e671723a 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -48,7 +48,11 @@ public bool MoveNext() if (entryIndex == -1 || entryIndex >= entryCount) { if (!chainEnumerator.MoveNext()) + { + entryIndex = int.MaxValue; + current = null; return false; + } ioContext.Reader.Seek(chainEnumerator.Current.StartOffset); entryIndex = 0; @@ -62,14 +66,14 @@ public bool MoveNext() /// /// Gets the for the specified stream ID. /// - public DirectoryEntry? GetDictionaryEntry(uint streamId) + public DirectoryEntry GetDictionaryEntry(uint streamId) { - if (streamId == StreamId.NoStream) - return null; + if (streamId > StreamId.Maximum) + throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}"); uint chainIndex = (uint)Math.DivRem(streamId, entryCount, out long entryIndex); if (!chainEnumerator.MoveTo(chainIndex)) - throw new ArgumentException("Invalid directory entry ID"); + throw new KeyNotFoundException($"Directory entry {streamId} was not found"); long position = chainEnumerator.Current.StartOffset + entryIndex * DirectoryEntry.Length; ioContext.Reader.Seek(position); @@ -82,6 +86,6 @@ public void Reset() { chainEnumerator.Reset(); entryIndex = -1; - current = default!; + current = null; } } diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 42d786ad..73c8ce2a 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -15,7 +15,8 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); - child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); + if (root.ChildId != StreamId.NoStream) + child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); PushLeft(child); } @@ -49,8 +50,12 @@ public bool MoveNext() } current = stack.Pop(); - DirectoryEntry? rightSibling = directoryEntryEnumerator.GetDictionaryEntry(Current.RightSiblingId); - PushLeft(rightSibling); + if (current.RightSiblingId != StreamId.NoStream) + { + DirectoryEntry rightSibling = directoryEntryEnumerator.GetDictionaryEntry(current.RightSiblingId); + PushLeft(rightSibling); + } + return true; } @@ -67,7 +72,7 @@ private void PushLeft(DirectoryEntry? node) while (node is not null) { stack.Push(node); - node = directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); + node = node.LeftSiblingId == StreamId.NoStream ? null : directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); } } } From 124d52792af457ccf5f2be88d4380f50a4d0c8c2 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 11:12:09 +1300 Subject: [PATCH 025/114] Improve MiniFatSectorEnumerator validation --- OpenMcdf3/MiniFatSectorEnumerator.cs | 48 ++++++++++++---------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index 277e9a38..f4be2b2c 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -8,14 +8,20 @@ namespace OpenMcdf3; internal sealed class MiniFatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly FatSectorChainEnumerator miniFatChain; + private readonly FatSectorChainEnumerator fatChain; bool start = true; MiniSector current = MiniSector.EndOfChain; public MiniFatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - miniFatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorId); + fatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorId); + } + + /// + public void Dispose() + { + fatChain.Dispose(); } /// @@ -49,20 +55,6 @@ public bool MoveNext() return !current.IsEndOfChain; } - public bool MoveTo(uint id) - { - if (id == SectorType.EndOfChain) - return false; - - while (start || current.Id < id) - { - if (!MoveNext()) - return false; - } - - return true; - } - /// public void Reset() { @@ -70,23 +62,25 @@ public void Reset() current = MiniSector.EndOfChain; } - public uint GetNextMiniFatSectorId(uint id) + /// + /// Gets the next mini FAT sector ID. + /// + /// + /// + /// + public uint GetNextMiniFatSectorId(uint sectorId) { - if (id > SectorType.Maximum) - throw new ArgumentException("Invalid sector ID"); + if (sectorId > SectorType.Maximum) + throw new ArgumentException($"Invalid sector ID: {sectorId}", nameof(sectorId)); int elementLength = ioContext.Header.SectorSize / sizeof(uint); - uint sectorId = (uint)Math.DivRem(id, elementLength, out long sectorOffset); - if (!miniFatChain.MoveTo(sectorId)) - throw new ArgumentException("Invalid sector ID"); + uint fatSectorId = (uint)Math.DivRem(sectorId, elementLength, out long sectorOffset); + if (!fatChain.MoveTo(fatSectorId)) + throw new ArgumentException($"Invalid sector ID: {sectorId}", nameof(sectorId)); - long position = miniFatChain.Current.StartOffset + sectorOffset * sizeof(uint); + long position = fatChain.Current.StartOffset + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); uint nextId = ioContext.Reader.ReadUInt32(); return nextId; } - - public void Dispose() - { - } } From 5f09487efafb82af01ed5d6b9b08c0548266d967 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 10:10:11 +1300 Subject: [PATCH 026/114] Improve exception detail --- OpenMcdf3/CfbBinaryReader.cs | 2 +- OpenMcdf3/DirectoryEntry.cs | 8 ++++---- OpenMcdf3/FatSectorEnumerator.cs | 4 ++-- OpenMcdf3/Storage.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index 7ddb13b2..6a931e2d 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -86,7 +86,7 @@ public NodeColor ReadColor() public DirectoryEntry ReadDirectoryEntry(Version version) { if (version is not Version.V3 and not Version.V4) - throw new ArgumentException($"Unsupported version: {version}"); + throw new ArgumentException($"Unsupported version: {version}", nameof(version)); DirectoryEntry entry = new(); Read(buffer, 0, DirectoryEntry.NameFieldLength); diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 2159e8a6..122e1a45 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -52,10 +52,10 @@ public string Name set { if (value.Contains(@"\") || value.Contains(@"/") || value.Contains(@":") || value.Contains(@"!")) - throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':','!'"); + throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':','!'", nameof(value)); if (Encoding.Unicode.GetByteCount(value) + 2 > NameFieldLength) - throw new ArgumentException($"{value} exceeds maximum encoded length of {NameFieldLength} bytes"); + throw new ArgumentException($"{value} exceeds maximum encoded length of {NameFieldLength} bytes", nameof(value)); name = value; } @@ -102,7 +102,7 @@ public DateTime CreationTime set { if (Type is StorageType.Stream or StorageType.Root && value != ZeroFileTime) - throw new ArgumentException("Creation time must be zero for streams and root"); + throw new ArgumentException("Creation time must be zero for streams and root", nameof(value)); creationTime = value; } @@ -117,7 +117,7 @@ public DateTime ModifiedTime set { if (Type is StorageType.Stream && value != ZeroFileTime) - throw new ArgumentException("Modified time must be zero for streams"); + throw new ArgumentException("Modified time must be zero for streams", nameof(value)); modifiedTime = value; } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 03cd07bb..c63f49d0 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -112,12 +112,12 @@ public void Reset() public uint GetNextFatSectorId(uint id) { if (id > SectorType.Maximum) - throw new ArgumentException("Invalid sector ID"); + throw new ArgumentException("Invalid sector ID", nameof(id)); int elementCount = ioContext.Header.SectorSize / sizeof(uint); uint sectorId = (uint)Math.DivRem(id, elementCount, out long sectorOffset); if (!MoveTo(sectorId)) - throw new ArgumentException("Invalid sector ID"); + throw new ArgumentException("Invalid sector ID", nameof(id)); long position = Current.StartOffset + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 444ddbdc..ac428018 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -50,7 +50,7 @@ public Storage OpenStorage(string name) this.ThrowIfDisposed(ioContext); DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Storage) - .FirstOrDefault(entry => entry.Name == name) ?? throw new DirectoryNotFoundException($"Directory not found {name}"); + .FirstOrDefault(entry => entry.Name == name) ?? throw new DirectoryNotFoundException($"Storage not found: {name}"); return new Storage(ioContext, entry); } @@ -59,7 +59,7 @@ public Stream OpenStream(string name) this.ThrowIfDisposed(ioContext); DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) - .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException("Stream not found", name); + .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException($"Stream not found: {name}", name); return entry.StreamLength switch { < Header.MiniStreamCutoffSize => new MiniFatStream(ioContext, entry), From b3d2995ea4c3c4c0f358c1659775b2c6199f391c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 11:30:45 +1300 Subject: [PATCH 027/114] Improve sector position terminology --- OpenMcdf3/CfbBinaryReader.cs | 2 +- OpenMcdf3/DirectoryEntryEnumerator.cs | 4 ++-- OpenMcdf3/FatSectorEnumerator.cs | 4 ++-- OpenMcdf3/FatStream.cs | 2 +- OpenMcdf3/IOContext.cs | 1 + OpenMcdf3/MiniFatSectorEnumerator.cs | 2 +- OpenMcdf3/MiniFatStream.cs | 2 +- OpenMcdf3/MiniSector.cs | 10 ++++++++-- OpenMcdf3/Sector.cs | 10 ++++++++-- 9 files changed, 25 insertions(+), 12 deletions(-) diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index 6a931e2d..7dc58662 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -118,5 +118,5 @@ public DirectoryEntry ReadDirectoryEntry(Version version) return entry; } - public void Seek(long offset) => BaseStream.Seek(offset, SeekOrigin.Begin); + public void Seek(long position) => BaseStream.Position = position; } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index e671723a..c84a41af 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -54,7 +54,7 @@ public bool MoveNext() return false; } - ioContext.Reader.Seek(chainEnumerator.Current.StartOffset); + ioContext.Reader.Seek(chainEnumerator.Current.Position); entryIndex = 0; } @@ -75,7 +75,7 @@ public DirectoryEntry GetDictionaryEntry(uint streamId) if (!chainEnumerator.MoveTo(chainIndex)) throw new KeyNotFoundException($"Directory entry {streamId} was not found"); - long position = chainEnumerator.Current.StartOffset + entryIndex * DirectoryEntry.Length; + long position = chainEnumerator.Current.Position + entryIndex * DirectoryEntry.Length; ioContext.Reader.Seek(position); current = ioContext.Reader.ReadDirectoryEntry(version); return current; diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index c63f49d0..e98e8d12 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -67,7 +67,7 @@ public bool MoveNext() int difatElementCount = ioContext.Header.SectorSize / sizeof(uint) - 1; Sector difatSector = new(difatSectorId, ioContext.Header.SectorSize); - long position = difatSector.StartOffset + difatSectorElementIndex * sizeof(uint); + long position = difatSector.Position + difatSectorElementIndex * sizeof(uint); ioContext.Reader.Seek(position); uint sectorId = ioContext.Reader.ReadUInt32(); current = new Sector(sectorId, ioContext.Header.SectorSize); @@ -119,7 +119,7 @@ public uint GetNextFatSectorId(uint id) if (!MoveTo(sectorId)) throw new ArgumentException("Invalid sector ID", nameof(id)); - long position = Current.StartOffset + sectorOffset * sizeof(uint); + long position = Current.Position + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); uint nextId = ioContext.Reader.ReadUInt32(); return nextId; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index eb526f73..f0270898 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -88,7 +88,7 @@ public override int Read(byte[] buffer, int offset, int count) Sector sector = chain.Current; int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); - ioContext.Reader.Seek(sector.StartOffset + sectorOffset); + ioContext.Reader.Seek(sector.Position + sectorOffset); int localOffset = offset + readCount; int read = ioContext.Reader.Read(buffer, localOffset, (int)readLength); if (read == 0) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 44b8de09..c9619a00 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -30,6 +30,7 @@ public IOContext(Header header, CfbBinaryReader reader, CfbBinaryWriter? writer, RootEntry = contextFlags.HasFlag(IOContextFlags.Create) ? new DirectoryEntry() : EnumerateDirectoryEntries().First(); + // TODO: Improve root directory entry validation } public void Dispose() diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index f4be2b2c..2e0a1fd8 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -78,7 +78,7 @@ public uint GetNextMiniFatSectorId(uint sectorId) if (!fatChain.MoveTo(fatSectorId)) throw new ArgumentException($"Invalid sector ID: {sectorId}", nameof(sectorId)); - long position = fatChain.Current.StartOffset + sectorOffset * sizeof(uint); + long position = fatChain.Current.Position + sectorOffset * sizeof(uint); ioContext.Reader.Seek(position); uint nextId = ioContext.Reader.ReadUInt32(); return nextId; diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index b7df0ac6..d51dd5c5 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -86,7 +86,7 @@ public override int Read(byte[] buffer, int offset, int count) MiniSector sector = chain.Current; int remaining = realCount - readCount; long readLength = Math.Min(remaining, buffer.Length); - fatStream.Position = sector.StartOffset + sectorOffset; + fatStream.Position = sector.Position + sectorOffset; int localOffset = offset + readCount; int read = fatStream.Read(buffer, localOffset, (int)readLength); if (read == 0) diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs index 38331d93..e30b6e81 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf3/MiniSector.cs @@ -14,7 +14,10 @@ internal record struct MiniSector(uint Id) public readonly bool IsEndOfChain => Id is SectorType.EndOfChain or SectorType.Free; - public readonly long StartOffset + /// + /// The position of the mini sector in the mini FAT stream. + /// + public readonly long Position { get { @@ -23,7 +26,10 @@ public readonly long StartOffset } } - public readonly long EndOffset + /// + /// The end position of the mini sector in the mini FAT stream. + /// + public readonly long EndPosition { get { diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index f3f64af1..94a9ae9a 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -17,7 +17,10 @@ internal record struct Sector(uint Id, int Length) public readonly bool IsValid => Id <= SectorType.Maximum; - public readonly long StartOffset + /// + /// The position of the mini sector in the compound file stream. + /// + public readonly long Position { get { @@ -26,7 +29,10 @@ public readonly long StartOffset } } - public readonly long EndOffset + /// + /// The end position of the mini sector in the compound file stream. + /// + public readonly long EndPosition { get { From 96ac4a5b4e09af4a9e38fe5bf0752aad8ea959db Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 17 Oct 2024 22:05:39 +1300 Subject: [PATCH 028/114] Improve stream read validation --- OpenMcdf3/FatStream.cs | 8 ++++---- OpenMcdf3/MiniFatStream.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index f0270898..7c86cda8 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -73,14 +73,14 @@ public override int Read(byte[] buffer, int offset, int count) if (count == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); - if (!chain.MoveTo(chainIndex)) - return 0; - int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); if (maxCount == 0) return 0; + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + return 0; + int realCount = Math.Min(count, maxCount); int readCount = 0; do diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index d51dd5c5..5044e953 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -71,14 +71,14 @@ public override int Read(byte[] buffer, int offset, int count) if (count == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); - if (!chain.MoveTo(chainIndex)) - return 0; - int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); if (maxCount == 0) return 0; + uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + return 0; + int realCount = Math.Min(count, maxCount); int readCount = 0; do From 305761f7899a562202c685379547194d73309ab6 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sat, 19 Oct 2024 20:35:51 +1300 Subject: [PATCH 029/114] Improve ReadGuid validation --- OpenMcdf3/CfbBinaryReader.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index 7dc58662..d237a598 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -7,6 +7,7 @@ namespace OpenMcdf3; /// internal sealed class CfbBinaryReader : BinaryReader { + readonly byte[] guidBuffer = new byte[16]; readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; public CfbBinaryReader(Stream input) @@ -14,7 +15,19 @@ public CfbBinaryReader(Stream input) { } - public Guid ReadGuid() => new(ReadBytes(16)); + public Guid ReadGuid() + { + int bytesRead = 0; + do + { + int n = Read(guidBuffer, bytesRead, guidBuffer.Length - bytesRead); + if (n == 0) + throw new EndOfStreamException(); + bytesRead += n; + } while (bytesRead < guidBuffer.Length); + + return new Guid(guidBuffer); + } public DateTime ReadFileTime() { @@ -22,8 +35,6 @@ public DateTime ReadFileTime() return DateTime.FromFileTimeUtc(fileTime); } - private void ReadBytes(byte[] buffer) => Read(buffer, 0, buffer.Length); - public Header ReadHeader() { Header header = new(); From 30de2e424f997d18d80e5a743783156a06f9a6be Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sat, 19 Oct 2024 22:36:22 +1300 Subject: [PATCH 030/114] Improved DirectoryEntry validation --- OpenMcdf3/CfbBinaryReader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index d237a598..ad365038 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -101,8 +101,9 @@ public DirectoryEntry ReadDirectoryEntry(Version version) DirectoryEntry entry = new(); Read(buffer, 0, DirectoryEntry.NameFieldLength); - int nameLength = Math.Max(0, ReadUInt16() - 2); - entry.Name = Encoding.Unicode.GetString(buffer, 0, nameLength); + ushort nameLength = ReadUInt16(); + int clampedNameLength = Math.Max(0, Math.Min(ushort.MaxValue, nameLength - 2)); + entry.Name = Encoding.Unicode.GetString(buffer, 0, clampedNameLength); entry.Type = ReadStorageType(); entry.Color = ReadColor(); entry.LeftSiblingId = ReadUInt32(); From 812d58296c77149a87609924f8c927b1d9823700 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sun, 20 Oct 2024 16:01:55 +1300 Subject: [PATCH 031/114] Refactor chain enumeration --- OpenMcdf3/FatSectorChainEnumerator.cs | 21 ++++++++++++++++++++- OpenMcdf3/FatSectorEnumerator.cs | 21 +++------------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatSectorChainEnumerator.cs index f06757a1..09117b00 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatSectorChainEnumerator.cs @@ -56,7 +56,7 @@ public bool MoveNext() } else if (!current.IsEndOfChain) { - uint sectorId = fatEnumerator.GetNextFatSectorId(current.Id); + uint sectorId = GetNextFatSectorId(current.Id); current = new(sectorId, ioContext.Header.SectorSize); Index++; } @@ -98,4 +98,23 @@ public void Reset() current = Sector.EndOfChain; Index = uint.MaxValue; } + + /// + /// Gets the next sector ID in the FAT chain. + /// + uint GetNextFatSectorId(uint id) + { + if (id > SectorType.Maximum) + throw new ArgumentException("Invalid sector ID", nameof(id)); + + int elementCount = ioContext.Header.SectorSize / sizeof(uint); + uint sectorId = (uint)Math.DivRem(id, elementCount, out long sectorOffset); + if (!fatEnumerator.MoveTo(sectorId)) + throw new ArgumentException("Invalid sector ID", nameof(id)); + + long position = fatEnumerator.Current.Position + sectorOffset * sizeof(uint); + ioContext.Reader.Seek(position); + uint nextId = ioContext.Reader.ReadUInt32(); + return nextId; + } } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index e98e8d12..21bbf81b 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -83,7 +83,9 @@ public bool MoveNext() return true; } - /// + /// + /// Moves the enumerator to the specified sector. + /// public bool MoveTo(uint sectorId) { if (sectorId < id) @@ -107,21 +109,4 @@ public void Reset() difatSectorElementIndex = 0; current = Sector.EndOfChain; } - - /// - public uint GetNextFatSectorId(uint id) - { - if (id > SectorType.Maximum) - throw new ArgumentException("Invalid sector ID", nameof(id)); - - int elementCount = ioContext.Header.SectorSize / sizeof(uint); - uint sectorId = (uint)Math.DivRem(id, elementCount, out long sectorOffset); - if (!MoveTo(sectorId)) - throw new ArgumentException("Invalid sector ID", nameof(id)); - - long position = Current.Position + sectorOffset * sizeof(uint); - ioContext.Reader.Seek(position); - uint nextId = ioContext.Reader.ReadUInt32(); - return nextId; - } } From ad09603ef5dc49defac010f5e520b5ab5b614296 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sun, 20 Oct 2024 20:49:04 +1300 Subject: [PATCH 032/114] Fix solution paths Seems VS Pro somehow added absolute paths to the projects --- OpenMcdf3.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 4f19d749..fbea60de 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3", "D:\OpenMcdf3\OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3", "OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Tests", "D:\OpenMcdf3\OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Tests", "OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34030FA7-0A06-43D1-85DD-ADD39D502C3C}" ProjectSection(SolutionItems) = preProject From a2ba07c87b78db3568d6e850c379dd5ca59c420c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 21 Oct 2024 10:15:23 +1300 Subject: [PATCH 033/114] Improve FatSectorEnumerator validation --- OpenMcdf3/FatSectorEnumerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 21bbf81b..f9530a12 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -88,6 +88,9 @@ public bool MoveNext() /// public bool MoveTo(uint sectorId) { + if (sectorId > SectorType.Maximum) + throw new ArgumentOutOfRangeException(nameof(sectorId)); + if (sectorId < id) Reset(); From 7391580afa634091bec3e0facd4a7e8c508527ac Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 21 Oct 2024 10:11:24 +1300 Subject: [PATCH 034/114] Rename chain enumerators --- OpenMcdf3/DirectoryEntryEnumerator.cs | 6 +++--- .../{FatSectorChainEnumerator.cs => FatChainEnumerator.cs} | 4 ++-- OpenMcdf3/FatStream.cs | 2 +- ...atSectorChainEnumerator.cs => MiniFatChainEnumerator.cs} | 4 ++-- OpenMcdf3/MiniFatSectorEnumerator.cs | 2 +- OpenMcdf3/MiniFatStream.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) rename OpenMcdf3/{FatSectorChainEnumerator.cs => FatChainEnumerator.cs} (95%) rename OpenMcdf3/{MiniFatSectorChainEnumerator.cs => MiniFatChainEnumerator.cs} (93%) diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index c84a41af..596c49ff 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -3,14 +3,14 @@ namespace OpenMcdf3; /// -/// Enumerates instances from a . +/// Enumerates instances from a . /// internal sealed class DirectoryEntryEnumerator : IEnumerator { private readonly IOContext ioContext; private readonly Version version; private readonly int entryCount; - private readonly FatSectorChainEnumerator chainEnumerator; + private readonly FatChainEnumerator chainEnumerator; private int entryIndex = -1; private DirectoryEntry? current; @@ -19,7 +19,7 @@ public DirectoryEntryEnumerator(IOContext ioContext) this.ioContext = ioContext; this.version = (Version)ioContext.Header.MajorVersion; this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; - this.chainEnumerator = new FatSectorChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + this.chainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } /// diff --git a/OpenMcdf3/FatSectorChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs similarity index 95% rename from OpenMcdf3/FatSectorChainEnumerator.cs rename to OpenMcdf3/FatChainEnumerator.cs index 09117b00..567b2cdb 100644 --- a/OpenMcdf3/FatSectorChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -5,7 +5,7 @@ namespace OpenMcdf3; /// /// Enumerates the s in a FAT sector chain. /// -internal sealed class FatSectorChainEnumerator : IEnumerator +internal sealed class FatChainEnumerator : IEnumerator { private readonly IOContext ioContext; private readonly FatSectorEnumerator fatEnumerator; @@ -13,7 +13,7 @@ internal sealed class FatSectorChainEnumerator : IEnumerator private bool start = true; private Sector current = Sector.EndOfChain; - public FatSectorChainEnumerator(IOContext ioContext, uint startSectorId) + public FatChainEnumerator(IOContext ioContext, uint startSectorId) { this.ioContext = ioContext; this.startId = startSectorId; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 7c86cda8..547b52fd 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -6,7 +6,7 @@ internal class FatStream : Stream { readonly IOContext ioContext; - readonly FatSectorChainEnumerator chain; + readonly FatChainEnumerator chain; readonly long length; long position; bool disposed; diff --git a/OpenMcdf3/MiniFatSectorChainEnumerator.cs b/OpenMcdf3/MiniFatChainEnumerator.cs similarity index 93% rename from OpenMcdf3/MiniFatSectorChainEnumerator.cs rename to OpenMcdf3/MiniFatChainEnumerator.cs index dc838d7d..b090cccc 100644 --- a/OpenMcdf3/MiniFatSectorChainEnumerator.cs +++ b/OpenMcdf3/MiniFatChainEnumerator.cs @@ -5,14 +5,14 @@ namespace OpenMcdf3; /// /// Enumerates the s in a Mini FAT sector chain. /// -internal sealed class MiniFatSectorChainEnumerator : IEnumerator +internal sealed class MiniFatChainEnumerator : IEnumerator { private readonly MiniFatSectorEnumerator miniFatEnumerator; private readonly uint startId; private bool start = true; private MiniSector current = MiniSector.EndOfChain; - public MiniFatSectorChainEnumerator(IOContext ioContext, uint startSectorId) + public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) { this.startId = startSectorId; miniFatEnumerator = new(ioContext); diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index 2e0a1fd8..ff41b421 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -8,7 +8,7 @@ namespace OpenMcdf3; internal sealed class MiniFatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly FatSectorChainEnumerator fatChain; + private readonly FatChainEnumerator fatChain; bool start = true; MiniSector current = MiniSector.EndOfChain; diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index 5044e953..915d5e88 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -6,7 +6,7 @@ internal sealed class MiniFatStream : Stream { readonly IOContext ioContext; - readonly MiniFatSectorChainEnumerator chain; + readonly MiniFatChainEnumerator chain; readonly FatStream fatStream; readonly long length; long position; From 3b68891316503def5cdab3352ccd67f071a9b2ec Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 21 Oct 2024 10:26:59 +1300 Subject: [PATCH 035/114] Add MiniFatEnumerator --- OpenMcdf3/MiniFatChainEnumerator.cs | 19 +++-- OpenMcdf3/MiniFatEnumerator.cs | 102 +++++++++++++++++++++++++++ OpenMcdf3/MiniFatSectorEnumerator.cs | 20 ++++++ 3 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 OpenMcdf3/MiniFatEnumerator.cs diff --git a/OpenMcdf3/MiniFatChainEnumerator.cs b/OpenMcdf3/MiniFatChainEnumerator.cs index b090cccc..a5a8e407 100644 --- a/OpenMcdf3/MiniFatChainEnumerator.cs +++ b/OpenMcdf3/MiniFatChainEnumerator.cs @@ -7,7 +7,7 @@ namespace OpenMcdf3; /// internal sealed class MiniFatChainEnumerator : IEnumerator { - private readonly MiniFatSectorEnumerator miniFatEnumerator; + private readonly MiniFatEnumerator miniFatEnumerator; private readonly uint startId; private bool start = true; private MiniSector current = MiniSector.EndOfChain; @@ -18,6 +18,12 @@ public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) miniFatEnumerator = new(ioContext); } + /// + public void Dispose() + { + miniFatEnumerator.Dispose(); + } + /// /// The index within the Mini FAT sector chain, or if the enumeration has not started. /// @@ -48,7 +54,7 @@ public bool MoveNext() } else if (!current.IsEndOfChain) { - uint sectorId = miniFatEnumerator.GetNextMiniFatSectorId(current.Id); + uint sectorId = GetNextMiniFatSectorId(current.Id); current = new(sectorId); Index++; } @@ -91,9 +97,12 @@ public void Reset() Index = uint.MaxValue; } - /// - public void Dispose() + /// + /// Gets the next sector ID in the FAT chain. + /// + uint GetNextMiniFatSectorId(uint id) { - miniFatEnumerator.Dispose(); + miniFatEnumerator.MoveTo(id); + return miniFatEnumerator.Current.Id; } } diff --git a/OpenMcdf3/MiniFatEnumerator.cs b/OpenMcdf3/MiniFatEnumerator.cs new file mode 100644 index 00000000..d1aa852c --- /dev/null +++ b/OpenMcdf3/MiniFatEnumerator.cs @@ -0,0 +1,102 @@ +using System.Collections; + +namespace OpenMcdf3; + +/// +/// Enumerates the s from the Mini FAT. +/// +internal sealed class MiniFatEnumerator : IEnumerator +{ + private readonly IOContext ioContext; + private readonly MiniFatSectorEnumerator miniFatSectorEnumerator; + private bool start = true; + private int index = int.MaxValue; + private MiniSector current = MiniSector.EndOfChain; + + public MiniFatEnumerator(IOContext ioContext) + { + miniFatSectorEnumerator = new(ioContext); + this.ioContext = ioContext; + } + + /// + public MiniSector Current + { + get + { + if (index == int.MaxValue) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if (start) + { + if (!miniFatSectorEnumerator.MoveNext()) + { + index = int.MaxValue; + return false; + } + + index = -1; + start = false; + } + + index++; + int elementCount = MiniSector.Length / sizeof(uint); + if (index > elementCount) + { + if (!miniFatSectorEnumerator.MoveNext()) + { + index = int.MaxValue; + return false; + } + + index = 0; + } + + long position = miniFatSectorEnumerator.Current.Position + index * sizeof(uint); + ioContext.Reader.Seek(position); + uint sectorId = ioContext.Reader.ReadUInt32(); + current = new(sectorId); + return true; + } + + public void MoveTo(uint id) + { + if (id > SectorType.Maximum) + throw new ArgumentException("Invalid sector ID", nameof(id)); + + int elementCount = ioContext.Header.SectorSize / sizeof(uint); + uint sectorId = (uint)Math.DivRem(id, elementCount, out long index); + + miniFatSectorEnumerator.MoveTo(sectorId); + long position = miniFatSectorEnumerator.Current.Position + index * sizeof(uint); + ioContext.Reader.Seek(position); + uint value = ioContext.Reader.ReadUInt32(); + this.index = (int)index; + start = false; + current = new(value); + } + + /// + public void Reset() + { + miniFatSectorEnumerator.Reset(); + start = true; + current = MiniSector.EndOfChain; + index = int.MaxValue; + } + + /// + public void Dispose() + { + miniFatSectorEnumerator.Dispose(); + } +} diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs index ff41b421..f9552d42 100644 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ b/OpenMcdf3/MiniFatSectorEnumerator.cs @@ -55,6 +55,26 @@ public bool MoveNext() return !current.IsEndOfChain; } + /// + /// Moves the enumerator to the specified sector. + /// + public bool MoveTo(uint sectorId) + { + if (sectorId > SectorType.Maximum) + throw new ArgumentOutOfRangeException(nameof(sectorId)); + + if (sectorId < current.Id) + Reset(); + + while (start || current.Id < sectorId) + { + if (!MoveNext()) + return false; + } + + return true; + } + /// public void Reset() { From e1b78accbd7caf63637da8857946a51e085777a7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 21 Oct 2024 11:11:38 +1300 Subject: [PATCH 036/114] Allow enumerating unallocated DirectoryEntries --- OpenMcdf3/DirectoryEntryEnumerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 596c49ff..19c8adba 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -60,7 +60,7 @@ public bool MoveNext() current = ioContext.Reader.ReadDirectoryEntry(version); entryIndex++; - return current.Type != StorageType.Unallocated; + return true; } /// From 5e19c7ff65db8ab56ebd3666d8a4d1fdbf4f4c54 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sat, 19 Oct 2024 21:12:06 +1300 Subject: [PATCH 037/114] Allow writing CFB streams --- OpenMcdf3.Benchmarks/InMemory.cs | 74 ++-- .../OpenMcdf3.Benchmarks.csproj | 3 +- OpenMcdf3.Benchmarks/Program.cs | 2 +- OpenMcdf3.Perf/OpenMcdf3.Perf.csproj | 14 + OpenMcdf3.Perf/Program.cs | 32 ++ OpenMcdf3.Tests/AssemblyInfo.cs | 19 + OpenMcdf3.Tests/BinaryWriterTests.cs | 71 ++++ OpenMcdf3.Tests/DebugWriter.cs | 15 + OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 7 +- OpenMcdf3.Tests/StorageTests.cs | 37 ++ OpenMcdf3.Tests/StreamTests.cs | 396 +++++++++++++++++- OpenMcdf3.Tests/TestStream_v3_0.cfs | Bin 1536 -> 1536 bytes OpenMcdf3.Tests/TestStream_v3_4095.cfs | Bin 6144 -> 6144 bytes OpenMcdf3.Tests/TestStream_v3_4096.cfs | Bin 5632 -> 10240 bytes OpenMcdf3.Tests/TestStream_v3_4097.cfs | Bin 6144 -> 10240 bytes OpenMcdf3.Tests/TestStream_v3_511.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_512.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_513.cfs | Bin 3072 -> 3072 bytes OpenMcdf3.Tests/TestStream_v3_63.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_64.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_65.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v3_65536.cfs | Bin 0 -> 68096 bytes OpenMcdf3.Tests/TestStream_v4_0.cfs | Bin 1536 -> 1536 bytes OpenMcdf3.Tests/TestStream_v4_4095.cfs | Bin 6144 -> 6144 bytes OpenMcdf3.Tests/TestStream_v4_4096.cfs | Bin 5632 -> 10240 bytes OpenMcdf3.Tests/TestStream_v4_4097.cfs | Bin 6144 -> 10240 bytes OpenMcdf3.Tests/TestStream_v4_511.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_512.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_513.cfs | Bin 3072 -> 3072 bytes OpenMcdf3.Tests/TestStream_v4_63.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_64.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.Tests/TestStream_v4_65.cfs | Bin 2560 -> 2560 bytes OpenMcdf3.sln | 8 +- OpenMcdf3/CfbBinaryReader.cs | 67 +-- OpenMcdf3/CfbBinaryWriter.cs | 30 +- OpenMcdf3/CfbStream.cs | 96 +++++ OpenMcdf3/DirectoryEntry.cs | 101 ++++- OpenMcdf3/DirectoryEntryEnumerator.cs | 105 +++-- OpenMcdf3/DirectoryTreeEnumerator.cs | 65 ++- OpenMcdf3/Fat.cs | 121 ++++++ OpenMcdf3/FatChainEntry.cs | 10 + OpenMcdf3/FatChainEnumerator.cs | 195 +++++++-- OpenMcdf3/FatEntry.cs | 13 + OpenMcdf3/FatEnumerator.cs | 132 ++++++ OpenMcdf3/FatSectorEnumerator.cs | 139 ++++-- OpenMcdf3/FatStream.cs | 93 +++- OpenMcdf3/Header.cs | 65 ++- OpenMcdf3/IOContext.cs | 107 ++++- OpenMcdf3/MiniFat.cs | 106 +++++ OpenMcdf3/MiniFatChainEnumerator.cs | 160 +++++-- OpenMcdf3/MiniFatEnumerator.cs | 114 ++--- OpenMcdf3/MiniFatSectorEnumerator.cs | 106 ----- OpenMcdf3/MiniFatStream.cs | 116 +++-- OpenMcdf3/MiniSector.cs | 8 +- OpenMcdf3/OpenMcdf3.csproj | 6 +- OpenMcdf3/RootStorage.cs | 22 +- OpenMcdf3/Sector.cs | 6 +- OpenMcdf3/SectorDataCache.cs | 25 ++ OpenMcdf3/SectorType.cs | 2 + OpenMcdf3/Storage.cs | 65 ++- OpenMcdf3/StreamExtensions.cs | 40 ++ OpenMcdf3/ThrowHelper.cs | 42 +- global.json | 7 + 63 files changed, 2342 insertions(+), 500 deletions(-) create mode 100644 OpenMcdf3.Perf/OpenMcdf3.Perf.csproj create mode 100644 OpenMcdf3.Perf/Program.cs create mode 100644 OpenMcdf3.Tests/AssemblyInfo.cs create mode 100644 OpenMcdf3.Tests/BinaryWriterTests.cs create mode 100644 OpenMcdf3.Tests/DebugWriter.cs create mode 100644 OpenMcdf3.Tests/TestStream_v3_65536.cfs create mode 100644 OpenMcdf3/CfbStream.cs create mode 100644 OpenMcdf3/Fat.cs create mode 100644 OpenMcdf3/FatChainEntry.cs create mode 100644 OpenMcdf3/FatEntry.cs create mode 100644 OpenMcdf3/FatEnumerator.cs create mode 100644 OpenMcdf3/MiniFat.cs delete mode 100644 OpenMcdf3/MiniFatSectorEnumerator.cs create mode 100644 OpenMcdf3/SectorDataCache.cs create mode 100644 OpenMcdf3/StreamExtensions.cs create mode 100644 global.json diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index 96bd180c..4d3297e5 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -18,11 +18,12 @@ public class InMemory : IDisposable private const string storageName = "MyStorage"; private const string streamName = "MyStream"; - private byte[] _readBuffer; + private byte[] readBuffer; - private MemoryStream _stream; + private readonly MemoryStream readStream = new(); + private readonly MemoryStream writeStream = new(); - [Params(Kb / 2, Kb, 4 * Kb, 128 * Kb, 256 * Kb, 512 * Kb, Kb * Kb)] + [Params(512, Mb /*Kb, 4 * Kb, 128 * Kb, 256 * Kb, 512 * Kb,*/)] public int BufferSize { get; set; } [Params(Mb /*, 8 * Mb, 64 * Mb, 128 * Mb*/)] @@ -30,68 +31,65 @@ public class InMemory : IDisposable public void Dispose() { - _stream?.Dispose(); + readStream?.Dispose(); + writeStream?.Dispose(); } [GlobalSetup] public void GlobalSetup() { - _stream = new MemoryStream(); - //_stream = File.Create("D:\\test.cfb"); - _readBuffer = new byte[BufferSize]; + readBuffer = new byte[BufferSize]; CreateFile(1); } - [GlobalCleanup] - public void GlobalCleanup() - { - _stream.Dispose(); - _stream = null; - _readBuffer = null; - } - [Benchmark] - public void Test() + public void Read() { - // - _stream.Seek(0L, SeekOrigin.Begin); - // - using var compoundFile = RootStorage.Open(_stream); - using Stream cfStream = compoundFile.OpenStream(streamName + 0); + using var compoundFile = RootStorage.Open(readStream); + using CfbStream cfStream = compoundFile.OpenStream(streamName + 0); long streamSize = cfStream.Length; long position = 0L; while (true) { if (position >= streamSize) break; - int read = cfStream.Read(_readBuffer, 0, _readBuffer.Length); + int read = cfStream.Read(readBuffer, 0, readBuffer.Length); position += read; if (read <= 0) break; } + } + + [Benchmark] + public void Write() + { + MemoryStream memoryStream = writeStream; + using var storage = RootStorage.Create(memoryStream); + Storage subStorage = storage.CreateStorage(storageName); + CfbStream stream = subStorage.CreateStream(streamName + 0); - //compoundFile.Close(); + while (stream.Length < TotalStreamSize) + { + stream.Write(readBuffer, 0, readBuffer.Length); + } } private void CreateFile(int streamCount) { - var iterationCount = TotalStreamSize / BufferSize; + int iterationCount = TotalStreamSize / BufferSize; - var buffer = new byte[BufferSize]; - Array.Fill(buffer, byte.MaxValue); + byte[] buffer = new byte[BufferSize]; + buffer.AsSpan().Fill(byte.MaxValue); const CFSConfiguration flags = CFSConfiguration.Default | CFSConfiguration.LeaveOpen; - using (var compoundFile = new CompoundFile(CFSVersion.Ver_3, flags)) + using var compoundFile = new CompoundFile(CFSVersion.Ver_3, flags); + CFStorage st = compoundFile.RootStorage; + for (int streamId = 0; streamId < streamCount; ++streamId) { - //var st = compoundFile.RootStorage.AddStorage(storageName); - var st = compoundFile.RootStorage; - for (var streamId = 0; streamId < streamCount; ++streamId) - { - var sm = st.AddStream(streamName + streamId); - - for (var iteration = 0; iteration < iterationCount; ++iteration) sm.Append(buffer); - } - - compoundFile.Save(_stream); - compoundFile.Close(); + CFStream sm = st.AddStream(streamName + streamId); + for (int iteration = 0; iteration < iterationCount; ++iteration) + sm.Append(buffer); } + + compoundFile.Save(readStream); + compoundFile.Close(); } } diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj index 94e38e90..8d732776 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj +++ b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj @@ -1,8 +1,9 @@  + net8.0 + 11.0 Exe - net8.0 enable enable diff --git a/OpenMcdf3.Benchmarks/Program.cs b/OpenMcdf3.Benchmarks/Program.cs index f00ccb3e..2707ca37 100644 --- a/OpenMcdf3.Benchmarks/Program.cs +++ b/OpenMcdf3.Benchmarks/Program.cs @@ -2,7 +2,7 @@ namespace OpenMcdf3.Benchmarks; -internal class Program +internal static class Program { private static void Main(string[] args) { diff --git a/OpenMcdf3.Perf/OpenMcdf3.Perf.csproj b/OpenMcdf3.Perf/OpenMcdf3.Perf.csproj new file mode 100644 index 00000000..9d82e145 --- /dev/null +++ b/OpenMcdf3.Perf/OpenMcdf3.Perf.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/OpenMcdf3.Perf/Program.cs b/OpenMcdf3.Perf/Program.cs new file mode 100644 index 00000000..63bb11d7 --- /dev/null +++ b/OpenMcdf3.Perf/Program.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +namespace OpenMcdf3.Perf; + +internal sealed class Program +{ + static void Main(string[] args) + { + var stopwatch = Stopwatch.StartNew(); + Write(Version.V3, 512, 1024); + Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); + } + + static void Write(Version version, int length, int iterations) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + //byte[] actualBuffer = new byte[length]; + + using MemoryStream memoryStream = new(2 * length); + using var rootStorage = RootStorage.Create(memoryStream, version); + using Stream stream = rootStorage.CreateStream("TestStream"); + + for (int i = 0; i < iterations; i++) + { + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + } +} diff --git a/OpenMcdf3.Tests/AssemblyInfo.cs b/OpenMcdf3.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..6df06f30 --- /dev/null +++ b/OpenMcdf3.Tests/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("38e3c8a7-44c7-4d13-9c30-7aa75b038c83")] + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/OpenMcdf3.Tests/BinaryWriterTests.cs b/OpenMcdf3.Tests/BinaryWriterTests.cs new file mode 100644 index 00000000..6e5f4722 --- /dev/null +++ b/OpenMcdf3.Tests/BinaryWriterTests.cs @@ -0,0 +1,71 @@ +namespace OpenMcdf3.Tests; + +[TestClass] +public sealed class BinaryWriterTests +{ + [TestMethod] + public void WriteGuid() + { + byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; + Guid expectedGuid = new(bytes); + using MemoryStream stream = new(bytes); + using (CfbBinaryWriter writer = new(stream)) + writer.Write(expectedGuid); + + stream.Position = 0; + using CfbBinaryReader reader = new(stream); + Guid actualGuid = reader.ReadGuid(); + + Assert.AreEqual(expectedGuid, actualGuid); + } + + [TestMethod] + [DataRow("TestStream_v3_0.cfs")] + [DataRow("TestStream_v4_0.cfs")] + public void WriteHeader(string fileName) + { + using FileStream stream = File.OpenRead(fileName); + using CfbBinaryReader reader = new(stream); + Header header = reader.ReadHeader(); + + using MemoryStream memoryStream = new(); + using CfbBinaryWriter writer = new(memoryStream); + writer.Write(header); + + memoryStream.Position = 0; + using CfbBinaryReader reader2 = new(memoryStream); + Header actualHeader = reader2.ReadHeader(); + + Assert.AreEqual(header, actualHeader); + } + + [TestMethod] + public void WriteDirectoryEntry() + { + DirectoryEntry expected = new() + { + Name = "Root Entry", + Type = StorageType.Storage, + Color = NodeColor.Red, + LeftSiblingId = 2, + RightSiblingId = 3, + ChildId = 4, + CLSID = Guid.NewGuid(), + StateBits = 5, + CreationTime = DateTime.UtcNow, + ModifiedTime = DateTime.UtcNow, + StartSectorId = 6, + StreamLength = 7 + }; + + using MemoryStream stream = new(); + using CfbBinaryWriter writer = new(stream); + writer.Write(expected); + + stream.Position = 0; + using CfbBinaryReader reader = new(stream); + DirectoryEntry actual = reader.ReadDirectoryEntry(Version.V4, 0); + + Assert.AreEqual(expected, actual); + } +} diff --git a/OpenMcdf3.Tests/DebugWriter.cs b/OpenMcdf3.Tests/DebugWriter.cs new file mode 100644 index 00000000..f795da38 --- /dev/null +++ b/OpenMcdf3.Tests/DebugWriter.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Text; + +namespace OpenMcdf3.Tests; + +internal sealed class DebugWriter : TextWriter +{ + public override Encoding Encoding => Encoding.Unicode; + + public override void Write(char value) => Debug.Write(value); + + public override void Write(string? value) => Debug.Write(value); + + public override void WriteLine(string? value) => Debug.WriteLine(value); +} diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 0a325a95..1a72e048 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net48;net8.0 Exe 11.0 enable @@ -14,7 +14,7 @@ - + @@ -68,6 +68,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 67e8c9d2..a4c1ea81 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -14,4 +14,41 @@ public void Read(string fileName, long storageCount) IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); Assert.AreEqual(storageCount, storageEntries.Count()); } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 1)] + [DataRow(Version.V3, 2)] + [DataRow(Version.V3, 4)] // Required 2 sectors including root + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 1)] + [DataRow(Version.V4, 2)] + [DataRow(Version.V4, 32)] // Required 2 sectors including root + public void CreateStorage(Version version, int subStorageCount) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + for (int i = 0; i < subStorageCount; i++) + rootStorage.CreateStorage($"Test{i}"); + } + + memoryStream.Position = 0; + using (var rootStorage = RootStorage.Open(memoryStream)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + Assert.AreEqual(subStorageCount, entries.Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void CreateDuplicateStorageThrowsException(Version version) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + rootStorage.CreateStorage("Test"); + Assert.ThrowsException(() => rootStorage.CreateStorage("Test")); + } } diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 01d1e369..5be7f8b3 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -14,6 +14,7 @@ public sealed class StreamTests [DataRow(Version.V3, 4095)] [DataRow(Version.V3, 4096)] [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 65536)] [DataRow(Version.V4, 0)] [DataRow(Version.V4, 63)] [DataRow(Version.V4, 64)] @@ -24,7 +25,7 @@ public sealed class StreamTests [DataRow(Version.V4, 4095)] [DataRow(Version.V4, 4096)] [DataRow(Version.V4, 4097)] - public void Read(Version version, int length) + public void ReadViaCopyTo(Version version, int length) { string fileName = $"TestStream_v{(int)version}_{length}.cfs"; using var rootStorage = RootStorage.OpenRead(fileName); @@ -41,4 +42,397 @@ public void Read(Version version, int length) StreamAssert.AreEqual(expectedStream, actualStream); } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 65536)] + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] + [DataRow(Version.V4, 4097)] + public void ReadSingleByte(Version version, int length) + { + string fileName = $"TestStream_v{(int)version}_{length}.cfs"; + using var rootStorage = RootStorage.OpenRead(fileName); + using Stream stream = rootStorage.OpenStream("TestStream"); + Assert.AreEqual(length, stream.Length); + + // Test files are filled with bytes equal to their position modulo 256 + using MemoryStream expectedStream = new(length); + for (int i = 0; i < length; i++) + expectedStream.WriteByte((byte)i); + + using MemoryStream actualStream = new(); + for (int i = 0; i < length; i++) + { + int value = stream.ReadByte(); + Assert.AreNotEqual(-1, value, "End of stream"); + actualStream.WriteByte((byte)value); + } + + StreamAssert.AreEqual(expectedStream, actualStream); + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void Write(Version version, int length) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("TestStream"); + Assert.AreEqual(0, stream.Length); + + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + Assert.AreEqual(length, stream.Length); + Assert.AreEqual(length, stream.Position); + + byte[] actualBuffer = new byte[length]; + stream.Position = 0; + stream.ReadExactly(actualBuffer); + + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void WriteThenRead(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + memoryStream.Position = 0; + using (var rootStorage = RootStorage.Open(memoryStream)) + { + using CfbStream stream = rootStorage.OpenStream("TestStream"); + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 256)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] + [DataRow(Version.V4, 4097)] + public void WriteMultiple(Version version, int length) + { + const int IterationCount = 2048; + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("TestStream"); + Assert.AreEqual(0, stream.Length); + + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + for (int i = 0; i < IterationCount; i++) + { + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + Assert.AreEqual(length * (i + 1), stream.Length); + } + + stream.Flush(); + + byte[] actualBuffer = new byte[length]; + stream.Position = 0; + for (int i = 0; i < IterationCount; i++) + { + actualBuffer.AsSpan().Clear(); + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 2* 64)] // Simplest case (1 sector => 2) + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 2 * 4096)] // Simplest case (1 sector => 2) + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void Shrink(Version version, int length) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("Test"); + + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + Assert.AreEqual(length, stream.Length); + + long baseStreamLength = memoryStream.Length; + + int newLength = length / 2; + stream.SetLength(newLength); + Assert.AreEqual(newLength, stream.Length); + + stream.Position = newLength; + stream.Write(expectedBuffer, newLength, expectedBuffer.Length - newLength); + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.Seek(0, SeekOrigin.Begin); + stream.ReadExactly(actualBuffer); + + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void MiniFatToFat(Version version) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("Test"); + + int length = 256; + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + int iterations = (int)Header.MiniStreamCutoffSize / length; + for (int i = 0; i < iterations; i++) + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + + Assert.AreEqual(length * iterations, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.Position = 0; + for (int i = 0; i < iterations; i++) + { + actualBuffer.AsSpan().Clear(); + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void FatToMiniFat(Version version) + { + const int length = 256; + + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("Test"); + + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + int iterations = (int)Header.MiniStreamCutoffSize / length; + for (int i = 0; i < iterations; i++) + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + + Assert.AreEqual(length * iterations, stream.Length); + + byte[] actualBuffer = new byte[length]; + + // Check reading from the regular sectors + stream.Position = 0; + for (int i = 0; i < iterations; i++) + { + actualBuffer.AsSpan().Clear(); + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + stream.SetLength(length); + Assert.AreEqual(length, stream.Length); + + stream.Position = 0; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] + [DataRow(Version.V4, 4097)] + public void CopyFromStream(Version version, int length) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("TestStream"); + Assert.AreEqual(0, stream.Length); + + // Fill with bytes equal to their position modulo 256 + using MemoryStream expectedStream = new(length); + for (int i = 0; i < length; i++) + expectedStream.WriteByte((byte)i); + + expectedStream.Position = 0; + expectedStream.CopyTo(stream); + Assert.AreEqual(length, stream.Length); + + using MemoryStream actualStream = new(); + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(actualStream); + + StreamAssert.AreEqual(expectedStream, actualStream); + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void CreateDuplicateStreamThrowsException(Version version) + { + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version); + using CfbStream stream = rootStorage.CreateStream("Test"); + Assert.ThrowsException(() => rootStorage.CreateStream("Test")); + } } diff --git a/OpenMcdf3.Tests/TestStream_v3_0.cfs b/OpenMcdf3.Tests/TestStream_v3_0.cfs index c3ee5ef6ddebb0ee8bec31dcf1ce2ac4d219681b..5052440a17b95ae8667d6ad50e3464851b2cbbd8 100644 GIT binary patch delta 66 zcmZqRY2cY)z{oh!P@07S2sR6{Y+z*l`~Uy{e?aDY#)%0$lbD>j7=Qv`z&JUONr8Pq PI=h|rZN|+dOg)SM9xfTz delta 58 zcmZqRY2cY)z{oJsP@092fq`MOAkzlMNi5D>jQ^p4aWW&T`sNar9!A!GK%u`t74I1* HCh!0NMIRS9 diff --git a/OpenMcdf3.Tests/TestStream_v3_4095.cfs b/OpenMcdf3.Tests/TestStream_v3_4095.cfs index effaf33da4b905593f2ce7e00c9f7b23284978bb..b95bc94c884c5cce3a1952ceb437bce7dbb1447f 100644 GIT binary patch delta 110 zcmZoLXfT*ypuorg1p*8VOh6`x|L_0*|1jarhC&+{dHw=LK%yW#nMXv1@gGq9JmbU! oj!9z9Tns=_FkqalC??OD(3Qb%r+u4|d2^{)H{&7^rbSEw0Kfz&Hvj+t delta 98 zcmZoLXfT*ypuoid1p*8VoIoZc5dZuC|365S1Bf>pGHqa7#33+=O@{!LaCQkUfcO7H?P9}x2c zF;E+WAP@@y@jsyA?TnK|oVgf)>cN0o&^04VG= Z*+qO0P{(=3i3x_AnEvxKGAv>e008lsEs6jD delta 157 zcmZn&XfT*ypuoid1p*8V|Nj5~58}aSP9S?TBijbXO&tIE8UIfXWG$YYz?y=D6Ezuu zYM6kS8Hibcm=%cGfS4VKIe-{s)?Xn02Q=t9<0KYmE=C{^3>YUfvZ_xmVQH9bz{0}B QC;&9yYZ4pJK%yYb0+iY;!1jT05|=X< p15f}A7$-Bb$a5Zen$2#feVdVab17Fh<0Lkg%`6;$7#A_I004f%Cl~+# delta 94 zcmZpWXpop-puoZa1p*8V%s?h15dZuC|365S35YifGHqa7#KAI&jfDp!{uhWr^kxC3 c4~&c0m?p6}b1?$>V8A$;kyU$h1xp7b0PpQ0oB#j- diff --git a/OpenMcdf3.Tests/TestStream_v3_63.cfs b/OpenMcdf3.Tests/TestStream_v3_63.cfs index eb2839c0790d255b8640c8031ecf739b41e53bba..652fd035935bf9e11d26bdf4cfc6756b5807b67c 100644 GIT binary patch delta 79 zcmZn=X%LxUz{oh!P@07S2sR6{Y+&U4`~Uy{e;~a1FY9;4NgU2x3_u_7Pdc(i<(-}!S1;3?((|4JFmOD^g6uI>prhbc72u` zW|;fOy}7?`cy{I)7Gzm=Sz-5mfA9CRZ*S6pU3*dv2LCvv2k}8Tk{FaJ_VGB*hl<{> z5(Gu>ha-_l(TBHjhIc*vPws&NzR&2-FIp*ncBLu$Gf$whlt|^M8dac*REa7R4}lOx(q8+E51)RTHqZ|Xzo)R+2Ee;PmoX%G#jAvBbR(Qq0;BWVm9|p`Un!SMrgF=3c$-&v3H~9*Q!|3 zJ3k>kZGWiv?~5#MaX#0ZS$N&q+#k_f82c$Xhzk{6fAnoEmn>iSNk{u`WBE#df6l+X z{@A~AY@hjGuD|$YJzw(ffgltYU#fJOgtCd{%2%jZsdAO1s@1C3NUm9{c1oSP_3AfB zZP=)Blcvp@w`kcat#zBW?b>(f*r{`uuHCx#=-I1xpY*={`VSa5Xz-As!-kI-IcoHn zvE#;PWM)m6IBD{fsne#Yz*tlu) zmaW^i@7Q_FvBw>M!iguHe9Eb(oqoodXPte{x#yjK!G#xHe95JkU4F%`E3dlxnrpAS z{)QWGx_S34x8Anr_B-yp>+XB*z3=`99(?HGM;?9b@h6^q>gi{meeU@eUVQ20z58By z^|jaEc=N5d-+6ccd+&en;emr6ef-I%pMCztmtTGT&9~ouf9QuFfBN~CUw`}kk3au9 zTo8_U`~CU*`1k+6zyJLG@83WF{`>sL=YKx`_4&WAe|-Jt>tA30yZyuMKW_hW`=8rC z-Tv$LZ@2%u|HJ)1?*DTCpZh=E|Lgv5_y4>8!Sx@me{ubf>z`cz<@z_*|GECr^`EYP zb^WjFpI!g$`ghm=d;EjPe|Y?h$NzZzlgEE~{F}%BdHkcte|r3@$Nzf#v&VmX{JY2h zJO9A>56-`E{)h8Vod4qd8|VKx|H%1I&cAa0m-Ek@|K|KV=l?nX(D{$fzjXem^G}`s z>ik>h|2qHJ`OnV3cK)~X&z=A7{CnsB>mTSp=wIl6=%47n=-=r7=pX4n>0jx8>7VJp z>EG%9=^yGp>R;-A>YwVr>fh@B>L2Sr>tE}C>!0htA1(+-f{^~Z{=5FW{=5FW{=5FW z{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW z{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW z{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW z{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW{=5FW z{=5FW{=5FW{=5FW{=5FW{=5ErasRy_5{c{xf?y|wIq#`t-2?jH`se!R`se!RCDT7Y E0w|`MRsaA1 literal 0 HcmV?d00001 diff --git a/OpenMcdf3.Tests/TestStream_v4_0.cfs b/OpenMcdf3.Tests/TestStream_v4_0.cfs index c3ee5ef6ddebb0ee8bec31dcf1ce2ac4d219681b..18971686e0ce07205e0882d8f230863d7a5dd0bd 100644 GIT binary patch delta 67 zcmZqRY2cY)z{oh!P@07S2sR6{Y+z*l`~Uy{e?aDY#)%0$lbD>j7=Qv`z&KfvNuDEN Qe+j#t_HD+^B}_ex05`50GXMYp delta 58 zcmZqRY2cY)z{oJsP@092fq`MOAkzlMNi5D>jQ^p4aWW&T`sNar9!A!GK%u`t74I1* HCh!0NMIRS9 diff --git a/OpenMcdf3.Tests/TestStream_v4_4095.cfs b/OpenMcdf3.Tests/TestStream_v4_4095.cfs index effaf33da4b905593f2ce7e00c9f7b23284978bb..6cef1929d9e69ec068a237b45863ed3c401c3cbc 100644 GIT binary patch delta 110 zcmZoLXfT*ypuorg1p*8VOh6`x|L_0*|1jarhC&+{dHw=LK%yW#nMXv1@gGq9JmbU! oj!9z9Tns=_FkqalC??Ojp{bPJPWv_^^X5{qZpK9-OpBNV0LGRmi~s-t delta 98 zcmZoLXfT*ypuoid1p*8VoIoZc5dZuC|365S1Bf>pGHqa7#33+=O@{!LaCQkUfcO7H?P9}x2c zF;E+WAP@@y@jsyA?TnK|oVgf)>cN0YUfvZ_xmVQH9bz{0}B QC;&9yYZ4pJpdlHl{^POaS0`Be(zn delta 72 zcmZn=X%LxUz{ot&P@092fq`MOAkzlMMI21L|Nj5~{}+fs^gp2Jd&Y?gEQ{EfCb2kk TF#^TFfN?S-tIp<1mUcz}JrE$A diff --git a/OpenMcdf3.Tests/TestStream_v4_512.cfs b/OpenMcdf3.Tests/TestStream_v4_512.cfs index 440560b20e37dc86d8af8ffdf89d1674b0dac0aa..58abae1433e03355910ad77481f446f66e024362 100644 GIT binary patch delta 82 zcmZn=X%LxUz{oh!P@07S2sR6{Y+&U5`~Uy{e=rObde1m9fn^ehGZzC;5DXY6D{{zl ZUT~~nx6{7O$h^6fqnmLN8`B~tCIH?ZBb5LE delta 72 zcmZn=X%LxUz{ot&P@092fq`MOAkzlMMI21L|Nj5~{}+fs^gp2Jd&Y?gEQ{EfCb2kk TF#^TFfN?S-tIp<1mUcz}JrE$A diff --git a/OpenMcdf3.Tests/TestStream_v4_513.cfs b/OpenMcdf3.Tests/TestStream_v4_513.cfs index 8c1f999a50206dd0836c92ae94b5fcbd3412ea57..740dda987624c24db8c5de5dcd7d6e123f790f6f 100644 GIT binary patch delta 110 zcmZpWXpop-puorg1p*8VOh6`x|L_0*|1jarf@~WY1^xm>K%yYb0+iY;!1jT05|=X< o15f}A7$-Bb$a5;NRV8A$;kyU$h1xp7b0PpQ0oB#j- diff --git a/OpenMcdf3.Tests/TestStream_v4_63.cfs b/OpenMcdf3.Tests/TestStream_v4_63.cfs index eb2839c0790d255b8640c8031ecf739b41e53bba..169a8470651051b3ed73175025edb2cc5e42573c 100644 GIT binary patch delta 80 zcmZn=X%LxUz{oh!P@07S2sR6{Y+&U4`~Uy{e;~a1FY9;4NgU2x3_u BaseStream.Position; + set => BaseStream.Position = value; + } + public Guid ReadGuid() { int bytesRead = 0; @@ -40,23 +46,21 @@ public Header ReadHeader() Header header = new(); Read(buffer, 0, Header.Signature.Length); if (!buffer.Take(Header.Signature.Length).SequenceEqual(Header.Signature)) - throw new FormatException("Invalid header signature"); + throw new FormatException("Invalid header signature."); header.CLSID = ReadGuid(); if (header.CLSID != Guid.Empty) - throw new FormatException($"Invalid header CLSID: {header.CLSID}"); + throw new FormatException($"Invalid header CLSID: {header.CLSID}."); header.MinorVersion = ReadUInt16(); header.MajorVersion = ReadUInt16(); if (header.MajorVersion is not (ushort)Version.V3 and not (ushort)Version.V4) - throw new FormatException($"Unsupported major version: {header.MajorVersion}"); + throw new FormatException($"Unsupported major version: {header.MajorVersion}."); else if (header.MinorVersion is not Header.ExpectedMinorVersion) - throw new FormatException($"Unsupported minor version: {header.MinorVersion}"); + throw new FormatException($"Unsupported minor version: {header.MinorVersion}."); ushort byteOrder = ReadUInt16(); if (byteOrder != Header.LittleEndian) - throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})"); + throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})."); header.SectorShift = ReadUInt16(); - ushort miniSectorShift = ReadUInt16(); - if (miniSectorShift != Header.MiniSectorShift) - throw new FormatException($"Unsupported sector shift {miniSectorShift}. Only {Header.MiniSectorShift} is supported"); + header.MiniSectorShift = ReadUInt16(); this.FillBuffer(6); header.DirectorySectorCount = ReadUInt32(); header.FatSectorCount = ReadUInt32(); @@ -64,7 +68,7 @@ public Header ReadHeader() this.FillBuffer(4); uint miniStreamCutoffSize = ReadUInt32(); if (miniStreamCutoffSize != Header.MiniStreamCutoffSize) - throw new FormatException("Mini stream cutoff size must be 4096 byte"); + throw new FormatException($"Mini stream cutoff size must be {Header.MiniStreamCutoffSize} bytes."); header.FirstMiniFatSectorId = ReadUInt32(); header.MiniFatSectorCount = ReadUInt32(); header.FirstDifatSectorId = ReadUInt32(); @@ -82,7 +86,7 @@ public StorageType ReadStorageType() { var type = (StorageType)ReadByte(); if (type is not StorageType.Storage and not StorageType.Stream and not StorageType.Root and not StorageType.Unallocated) - throw new FormatException($"Invalid storage type: {type}"); + throw new FormatException($"Invalid storage type: {type}."); return type; } @@ -90,36 +94,41 @@ public NodeColor ReadColor() { var color = (NodeColor)ReadByte(); if (color is not NodeColor.Black and not NodeColor.Red) - throw new FormatException($"Invalid node color: {color}"); + throw new FormatException($"Invalid node color: {color}."); return color; } - public DirectoryEntry ReadDirectoryEntry(Version version) + public DirectoryEntry ReadDirectoryEntry(Version version, uint sid) { if (version is not Version.V3 and not Version.V4) - throw new ArgumentException($"Unsupported version: {version}", nameof(version)); + throw new ArgumentException($"Unsupported version: {version}.", nameof(version)); - DirectoryEntry entry = new(); - Read(buffer, 0, DirectoryEntry.NameFieldLength); + Read(buffer, 0, DirectoryEntry.NameFieldLength); // TODO ushort nameLength = ReadUInt16(); - int clampedNameLength = Math.Max(0, Math.Min(ushort.MaxValue, nameLength - 2)); - entry.Name = Encoding.Unicode.GetString(buffer, 0, clampedNameLength); - entry.Type = ReadStorageType(); - entry.Color = ReadColor(); - entry.LeftSiblingId = ReadUInt32(); - entry.RightSiblingId = ReadUInt32(); - entry.ChildId = ReadUInt32(); - entry.CLSID = ReadGuid(); - entry.StateBits = ReadUInt32(); - entry.CreationTime = ReadFileTime(); - entry.ModifiedTime = ReadFileTime(); - entry.StartSectorId = ReadUInt32(); + int clampedNameLength = Math.Max(0, Math.Min(DirectoryEntry.NameFieldLength, nameLength - 2)); + string name = Encoding.Unicode.GetString(buffer, 0, clampedNameLength); + + DirectoryEntry entry = new() + { + Id = sid, + Name = name, + Type = ReadStorageType(), + Color = ReadColor(), + LeftSiblingId = ReadUInt32(), + RightSiblingId = ReadUInt32(), + ChildId = ReadUInt32(), + CLSID = ReadGuid(), + StateBits = ReadUInt32(), + CreationTime = ReadFileTime(), + ModifiedTime = ReadFileTime(), + StartSectorId = ReadUInt32() + }; if (version == Version.V3) { entry.StreamLength = ReadUInt32(); if (entry.StreamLength > DirectoryEntry.MaxV3StreamLength) - throw new FormatException($"Stream length {entry.StreamLength} exceeds maximum value {DirectoryEntry.MaxV3StreamLength}"); + throw new FormatException($"Stream length {entry.StreamLength} exceeds maximum value {DirectoryEntry.MaxV3StreamLength}."); ReadUInt32(); // Skip unused 4 bytes } else if (version == Version.V4) @@ -129,6 +138,4 @@ public DirectoryEntry ReadDirectoryEntry(Version version) return entry; } - - public void Seek(long position) => BaseStream.Position = position; } diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index f8f08e0a..b041469e 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -14,11 +14,26 @@ public CfbBinaryWriter(Stream input) { } + public long Position + { + get => BaseStream.Position; + set => BaseStream.Position = value; + } + +#if NETSTANDARD2_1_OR_GREATER + public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); +#endif + public void Write(Guid value) { - // TODO: Avoid heap allocation +#if NETSTANDARD2_1_OR_GREATER + Span localBuffer = stackalloc byte[16]; + value.TryWriteBytes(localBuffer); + Write(localBuffer); +#else byte[] bytes = value.ToByteArray(); - Write(bytes, 0, bytes.Length); + Write(bytes); +#endif } public void Write(DateTime value) @@ -27,8 +42,6 @@ public void Write(DateTime value) Write(fileTime); } - private void WriteBytes(byte[] buffer) => Write(buffer, 0, buffer.Length); - public void Write(Header header) { Write(Header.Signature); @@ -37,8 +50,8 @@ public void Write(Header header) Write(header.MajorVersion); Write(Header.LittleEndian); Write(header.SectorShift); - Write(Header.MiniSectorShift); - WriteBytes(Header.Unused); + Write(header.MiniSectorShift); + Write(Header.Unused); Write(header.DirectorySectorCount); Write(header.FatSectorCount); Write(header.FirstDirectorySectorId); @@ -48,13 +61,16 @@ public void Write(Header header) Write(header.MiniFatSectorCount); Write(header.FirstDifatSectorId); Write(header.DifatSectorCount); + for (int i = 0; i < Header.DifatArrayLength; i++) + Write(header.Difat[i]); } public void Write(DirectoryEntry entry) { + buffer.AsSpan().Clear(); int nameLength = Encoding.Unicode.GetBytes(entry.Name, 0, entry.Name.Length, buffer, 0); - Write(nameLength); Write(buffer, 0, DirectoryEntry.NameFieldLength); + Write((short)(nameLength + 2)); Write((byte)entry.Type); Write((byte)entry.Color); Write(entry.LeftSiblingId); diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs new file mode 100644 index 00000000..473eae7f --- /dev/null +++ b/OpenMcdf3/CfbStream.cs @@ -0,0 +1,96 @@ +namespace OpenMcdf3; + +/// +/// Represents a stream in a compound file. +/// +public sealed class CfbStream : Stream +{ + private readonly IOContext ioContext; + private readonly DirectoryEntry directoryEntry; + private Stream stream; + + internal CfbStream(IOContext ioContext, DirectoryEntry directoryEntry) + { + this.ioContext = ioContext; + this.directoryEntry = directoryEntry; + stream = directoryEntry.StreamLength < Header.MiniStreamCutoffSize + ? new MiniFatStream(ioContext, directoryEntry) + : new FatStream(ioContext, directoryEntry); + } + + protected override void Dispose(bool disposing) + { + stream.Dispose(); + + base.Dispose(disposing); + } + + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position { get => stream.Position; set => stream.Position = value; } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + + public override void SetLength(long value) + { + this.ThrowIfNotWritable(); + + if (value >= Header.MiniStreamCutoffSize && stream is MiniFatStream miniStream) + { + long position = miniStream.Position; + miniStream.Position = 0; + + DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); + FatStream fatStream = new(ioContext, newDirectoryEntry); + fatStream.SetLength(value); // Reserve enough space up front + miniStream.CopyTo(fatStream); + fatStream.Position = position; + stream = fatStream; + + miniStream.SetLength(0); + miniStream.Dispose(); + } + else if (value < Header.MiniStreamCutoffSize && stream is FatStream fatStream) + { + long position = fatStream.Position; + fatStream.Position = 0; + + DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); + MiniFatStream miniFatStream = new(ioContext, newDirectoryEntry); + fatStream.SetLength(value); // Truncate the stream + fatStream.CopyTo(miniFatStream); + miniFatStream.Position = position; + stream = miniFatStream; + + fatStream.SetLength(0); + fatStream.Dispose(); + } + else + { + stream.SetLength(value); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + + this.ThrowIfNotWritable(); + + long newPosition = Position + count; + if (newPosition > stream.Length) + SetLength(newPosition); + + stream.Write(buffer, offset, count); + } +} diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 122e1a45..6bdc87be 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -1,6 +1,4 @@ -using System.Text; - -namespace OpenMcdf3; +namespace OpenMcdf3; /// /// The storage type of a . @@ -34,7 +32,7 @@ internal static class StreamId /// /// Encapsulates data about a or Stream. /// -internal sealed class DirectoryEntry +internal sealed class DirectoryEntry : IEquatable { internal const int Length = 128; internal const int NameFieldLength = 64; @@ -42,21 +40,20 @@ internal sealed class DirectoryEntry internal static readonly DateTime ZeroFileTime = DateTime.FromFileTimeUtc(0); + internal static readonly byte[] Unallocated = new byte[128]; + string name = string.Empty; DateTime creationTime; DateTime modifiedTime; + public uint Id { get; set; } + public string Name { get => name; set { - if (value.Contains(@"\") || value.Contains(@"/") || value.Contains(@":") || value.Contains(@"!")) - throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':','!'", nameof(value)); - - if (Encoding.Unicode.GetByteCount(value) + 2 > NameFieldLength) - throw new ArgumentException($"{value} exceeds maximum encoded length of {NameFieldLength} bytes", nameof(value)); - + ThrowHelper.ThrowIfNameIsInvalid(value); name = value; } } @@ -71,17 +68,17 @@ public string Name /// /// Stream ID of the left sibling. /// - public uint LeftSiblingId { get; set; } + public uint LeftSiblingId { get; set; } = StreamId.NoStream; /// /// Stream ID of the right sibling. /// - public uint RightSiblingId { get; set; } + public uint RightSiblingId { get; set; } = StreamId.NoStream; /// /// Stream ID of the child. /// - public uint ChildId { get; set; } + public uint ChildId { get; set; } = StreamId.NoStream; /// /// GUID for storage objects. @@ -102,7 +99,7 @@ public DateTime CreationTime set { if (Type is StorageType.Stream or StorageType.Root && value != ZeroFileTime) - throw new ArgumentException("Creation time must be zero for streams and root", nameof(value)); + throw new ArgumentException("Creation time must be zero for streams and root.", nameof(value)); creationTime = value; } @@ -117,7 +114,7 @@ public DateTime ModifiedTime set { if (Type is StorageType.Stream && value != ZeroFileTime) - throw new ArgumentException("Modified time must be zero for streams", nameof(value)); + throw new ArgumentException("Modified time must be zero for streams.", nameof(value)); modifiedTime = value; } @@ -126,14 +123,84 @@ public DateTime ModifiedTime /// /// The starting sector location for a stream or the first sector of the mini-stream for the root storage object. /// - public uint StartSectorId { get; set; } + public uint StartSectorId { get; set; } = StreamId.NoStream; /// /// The length of the stream. /// public long StreamLength { get; set; } + public override bool Equals(object? obj) => Equals(obj as DirectoryEntry); + + public bool Equals(DirectoryEntry? other) + { + return other is not null + && Name == other.Name + && Type == other.Type + && Color == other.Color + && LeftSiblingId == other.LeftSiblingId + && RightSiblingId == other.RightSiblingId + && ChildId == other.ChildId + && CLSID == other.CLSID + && StateBits == other.StateBits + && CreationTime == other.CreationTime + && ModifiedTime == other.ModifiedTime + && StartSectorId == other.StartSectorId + && StreamLength == other.StreamLength; + } + + public void RecycleRoot() => Recycle(StorageType.Root, "Root Entry"); + + public void Recycle(StorageType storageType, string name) + { + Type = storageType; + Color = NodeColor.Black; + Name = name; + LeftSiblingId = StreamId.NoStream; + RightSiblingId = StreamId.NoStream; + ChildId = StreamId.NoStream; + StartSectorId = StreamId.NoStream; + StreamLength = 0; + + if (storageType is StorageType.Root) + { + CreationTime = ZeroFileTime; + ModifiedTime = DateTime.UtcNow; + } + if (storageType is StorageType.Storage) + { + DateTime now = DateTime.UtcNow; + CreationTime = now; + ModifiedTime = now; + } + else + { + CreationTime = ZeroFileTime; + ModifiedTime = ZeroFileTime; + } + } + public EntryInfo ToEntryInfo() => new() { Name = Name }; - public override string ToString() => Name; + public override string ToString() => $"{Id}: \"{Name}\""; + + public DirectoryEntry Clone() + { + return new DirectoryEntry + { + Id = Id, + Name = Name, + Type = Type, + Color = Color, + LeftSiblingId = LeftSiblingId, + RightSiblingId = RightSiblingId, + ChildId = ChildId, + CLSID = CLSID, + StateBits = StateBits, + CreationTime = CreationTime, + ModifiedTime = ModifiedTime, + StartSectorId = StreamId.NoStream, + StreamLength = 0 + }; + } } diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 19c8adba..9e78f0de 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -8,26 +8,25 @@ namespace OpenMcdf3; internal sealed class DirectoryEntryEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly Version version; - private readonly int entryCount; - private readonly FatChainEnumerator chainEnumerator; - private int entryIndex = -1; + private readonly FatChainEnumerator fatChainEnumerator; + private bool start = true; + private uint index = uint.MaxValue; private DirectoryEntry? current; public DirectoryEntryEnumerator(IOContext ioContext) { this.ioContext = ioContext; - this.version = (Version)ioContext.Header.MajorVersion; - this.entryCount = ioContext.Header.SectorSize / DirectoryEntry.Length; - this.chainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + this.fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } /// public void Dispose() { - chainEnumerator.Dispose(); + fatChainEnumerator.Dispose(); } + private int EntriesPerSector => ioContext.SectorSize / DirectoryEntry.Length; + /// public DirectoryEntry Current { @@ -45,47 +44,97 @@ public DirectoryEntry Current /// public bool MoveNext() { - if (entryIndex == -1 || entryIndex >= entryCount) + if (start) { - if (!chainEnumerator.MoveNext()) - { - entryIndex = int.MaxValue; - current = null; - return false; - } + start = false; + index = 0; + } - ioContext.Reader.Seek(chainEnumerator.Current.Position); - entryIndex = 0; + uint chainIndex = (uint)Math.DivRem(index, EntriesPerSector, out long entryIndex); + if (!fatChainEnumerator.MoveTo(chainIndex)) + { + current = null; + index = uint.MaxValue; + return false; } - current = ioContext.Reader.ReadDirectoryEntry(version); - entryIndex++; + ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, index); + index++; return true; } + public DirectoryEntry CreateOrRecycleDirectoryEntry() + { + DirectoryEntry? entry = TryRecycleDirectoryEntry(); + if (entry is not null) + return entry; + + CfbBinaryWriter writer = ioContext.Writer; + uint id = fatChainEnumerator.Extend(); + if (ioContext.Header.FirstDirectorySectorId == SectorType.EndOfChain) + ioContext.Header.FirstDirectorySectorId = id; + + Sector sector = new(id, ioContext.SectorSize); + writer.Position = sector.Position; + int directoryEntriesPerSector = EntriesPerSector; + for (int i = 0; i < directoryEntriesPerSector; i++) + writer.Write(DirectoryEntry.Unallocated); + + entry = TryRecycleDirectoryEntry() + ?? throw new InvalidOperationException("Failed to add or recycle directory entry."); + return entry; + } + + private DirectoryEntry? TryRecycleDirectoryEntry() + { + Reset(); + + while (MoveNext()) + { + if (current!.Type == StorageType.Unallocated) + { + return current; + } + } + + return null; + } + /// /// Gets the for the specified stream ID. /// public DirectoryEntry GetDictionaryEntry(uint streamId) { if (streamId > StreamId.Maximum) - throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}"); + throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}.", nameof(streamId)); - uint chainIndex = (uint)Math.DivRem(streamId, entryCount, out long entryIndex); - if (!chainEnumerator.MoveTo(chainIndex)) - throw new KeyNotFoundException($"Directory entry {streamId} was not found"); + uint chainIndex = (uint)Math.DivRem(streamId, EntriesPerSector, out long entryIndex); + if (!fatChainEnumerator.MoveTo(chainIndex)) + throw new KeyNotFoundException($"Directory entry {streamId} was not found."); - long position = chainEnumerator.Current.Position + entryIndex * DirectoryEntry.Length; - ioContext.Reader.Seek(position); - current = ioContext.Reader.ReadDirectoryEntry(version); + ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, streamId); return current; } + public void Write(DirectoryEntry entry) + { + uint chainIndex = (uint)Math.DivRem(entry.Id, EntriesPerSector, out long entryIndex); + if (!fatChainEnumerator.MoveTo(chainIndex)) + throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); + + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + writer.Write(entry); + } + /// public void Reset() { - chainEnumerator.Reset(); - entryIndex = -1; + fatChainEnumerator.Reset(); + start = true; current = null; + index = uint.MaxValue; } } diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 73c8ce2a..f5a7490f 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; namespace OpenMcdf3; @@ -7,7 +8,9 @@ namespace OpenMcdf3; /// internal sealed class DirectoryTreeEnumerator : IEnumerator { - private readonly DirectoryEntry? child; + private readonly IOContext ioContext; + private readonly DirectoryEntry root; + private DirectoryEntry? child; private readonly Stack stack = new(); private readonly DirectoryEntryEnumerator directoryEntryEnumerator; DirectoryEntry? current; @@ -15,6 +18,8 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); + this.ioContext = ioContext; + this.root = root; if (root.ChildId != StreamId.NoStream) child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); PushLeft(child); @@ -75,4 +80,62 @@ private void PushLeft(DirectoryEntry? node) node = node.LeftSiblingId == StreamId.NoStream ? null : directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); } } + + public bool MoveTo(StorageType type, string name) + { + Reset(); + + while (MoveNext()) + { + if (Current.Type == type && Current.Name == name) + return true; + } + + return false; + } + + public DirectoryEntry? TryGetDirectoryEntry(StorageType type, string name) + { + if (MoveTo(type, name)) + return Current; + return null; + } + + public DirectoryEntry Add(StorageType storageType, string name) + { + if (MoveTo(storageType, name)) + throw new IOException($"{storageType} \"{name}\" already exists."); + + DirectoryEntry entry = directoryEntryEnumerator.CreateOrRecycleDirectoryEntry(); + entry.Recycle(storageType, name); + + Add(entry); + + return entry; + } + + void Add(DirectoryEntry entry) + { + Reset(); + + entry.Color = NodeColor.Black; + directoryEntryEnumerator.Write(entry); + + if (root.ChildId == StreamId.NoStream) + { + Debug.Assert(child is null); + root.ChildId = entry.Id; + directoryEntryEnumerator.Write(root); + child = entry; + } + else + { + Debug.Assert(child is not null); + DirectoryEntry node = child!; + while (node.LeftSiblingId != StreamId.NoStream) + node = directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); + node.LeftSiblingId = entry.Id; + directoryEntryEnumerator.Write(node); + } + } } diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs new file mode 100644 index 00000000..5e2ec4c8 --- /dev/null +++ b/OpenMcdf3/Fat.cs @@ -0,0 +1,121 @@ +using System.Collections; +using System.Diagnostics; + +namespace OpenMcdf3; + +/// +/// Encapsulates getting and setting entries in the FAT. +/// +internal sealed class Fat : IEnumerable, IDisposable +{ + private readonly IOContext ioContext; + private readonly FatSectorEnumerator fatSectorEnumerator; + + internal int FatElementsPerSector => ioContext.SectorSize / sizeof(uint); + + internal int DifatElementsPerSector => ioContext.SectorSize / sizeof(uint) - 1; + + public Fat(IOContext ioContext) + { + this.ioContext = ioContext; + fatSectorEnumerator = new(ioContext); + } + + public void Dispose() + { + fatSectorEnumerator.Dispose(); + } + + public uint this[uint key] + { + get + { + if (!TryGetValue(key, out uint value)) + throw new KeyNotFoundException($"FAT index not found: {key}."); + return value; + + } + set + { + if (!TrySetValue(key, value)) + throw new KeyNotFoundException($"FAT index not found: {key}."); + } + } + + uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) + { + int DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; + if (key < DifatArrayElementCount) + return (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); + + return (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); + } + + public bool TryGetValue(uint key, out uint value) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(key); + + uint sectorId = GetSectorIndexAndElementOffset(key, out long elementIndex); + bool ok = fatSectorEnumerator.MoveTo(sectorId); + if (!ok) + { + value = uint.MaxValue; + return false; + } + + CfbBinaryReader reader = ioContext.Reader; + reader.Position = fatSectorEnumerator.Current.Position + elementIndex * sizeof(uint); + value = reader.ReadUInt32(); + return true; + } + + public bool TrySetValue(uint key, uint value) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(key); + + uint fatSectorIndex = GetSectorIndexAndElementOffset(key, out long elementIndex); + if (!fatSectorEnumerator.MoveTo(fatSectorIndex)) + return false; + + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatSectorEnumerator.Current.Position + elementIndex * sizeof(uint); + writer.Write(value); + return true; + } + + /// + /// Adds a new entry to the FAT. + /// + /// The index of the new entry in the FAT + public uint Add(FatEnumerator fatEnumerator, uint startIndex) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(startIndex); + + bool movedToFreeEntry = fatEnumerator.MoveTo(startIndex) && fatEnumerator.MoveNextFreeEntry(); + if (!movedToFreeEntry) + { + uint newSectorId = fatSectorEnumerator.Add(); + + bool ok = fatEnumerator.MoveTo(newSectorId); + Debug.Assert(ok); + + ok = fatEnumerator.MoveNextFreeEntry(); + Debug.Assert(ok); + } + + FatEntry entry = fatEnumerator.Current; + ioContext.ExtendStreamLength(fatEnumerator.CurrentSector.EndPosition); + this[entry.Index] = SectorType.EndOfChain; + return entry.Index; + } + + public IEnumerator GetEnumerator() => new FatEnumerator(ioContext); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal void Trace(TextWriter writer) + { + using FatEnumerator fatEnumerator = new(ioContext); + fatEnumerator.Trace(writer); + } +} diff --git a/OpenMcdf3/FatChainEntry.cs b/OpenMcdf3/FatChainEntry.cs new file mode 100644 index 00000000..ba7302fc --- /dev/null +++ b/OpenMcdf3/FatChainEntry.cs @@ -0,0 +1,10 @@ +namespace OpenMcdf3; + +internal record struct FatChainEntry(uint Index, uint Value) +{ + internal static readonly FatChainEntry Invalid = new(uint.MaxValue, SectorType.EndOfChain); + + public readonly bool IsFreeOrEndOfChain => SectorType.IsFreeOrEndOfChain(Value); + + public override readonly string ToString() => $"#{Index}: {Value}"; +} diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index 567b2cdb..1ccae887 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -1,17 +1,20 @@ using System.Collections; +using System.Diagnostics; namespace OpenMcdf3; /// /// Enumerates the s in a FAT sector chain. /// -internal sealed class FatChainEnumerator : IEnumerator +internal sealed class FatChainEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly FatSectorEnumerator fatEnumerator; - private readonly uint startId; + private readonly FatEnumerator fatEnumerator; + private uint startId; private bool start = true; - private Sector current = Sector.EndOfChain; + private uint index = uint.MaxValue; + private FatChainEntry current = FatChainEntry.Invalid; + private long length = -1; public FatChainEnumerator(IOContext ioContext, uint startSectorId) { @@ -26,17 +29,16 @@ public void Dispose() fatEnumerator.Dispose(); } - /// - /// The index within the FAT sector chain, or if the enumeration has not started. - /// - public uint Index { get; private set; } = uint.MaxValue; + public uint StartId => startId; + + public Sector CurrentSector => new(Current.Value, ioContext.SectorSize); /// - public Sector Current + public FatChainEntry Current { get { - if (current.IsEndOfChain) + if (index == uint.MaxValue) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); return current; } @@ -50,24 +52,44 @@ public bool MoveNext() { if (start) { - current = new(startId, ioContext.Header.SectorSize); - Index = 0; + if (startId is SectorType.EndOfChain or SectorType.Free) + { + index = uint.MaxValue; + current = FatChainEntry.Invalid; + return false; + } + + index = 0; + current = new(index, startId); start = false; + return true; } - else if (!current.IsEndOfChain) + + if (current.IsFreeOrEndOfChain || current == FatChainEntry.Invalid) { - uint sectorId = GetNextFatSectorId(current.Id); - current = new(sectorId, ioContext.Header.SectorSize); - Index++; + index = uint.MaxValue; + current = FatChainEntry.Invalid; + return false; } - if (current.IsEndOfChain) + uint value = ioContext.Fat[current.Value]; + if (value is SectorType.EndOfChain) { - current = Sector.EndOfChain; - Index = uint.MaxValue; + index = uint.MaxValue; + current = FatChainEntry.Invalid; return false; } + index++; + if (index > SectorType.Maximum) + { + // If the index is greater than the maximum, then the chain must contain a loop + index = uint.MaxValue; + current = FatChainEntry.Invalid; + throw new IOException("FAT sector chain is corrupt"); + } + + current = new(index, value); return true; } @@ -78,10 +100,10 @@ public bool MoveNext() /// true if the enumerator was successfully advanced to the given index public bool MoveTo(uint index) { - if (index < Index) + if (index < this.index) Reset(); - while (start || Index < index) + while (start || this.index < index) { if (!MoveNext()) return false; @@ -90,31 +112,122 @@ public bool MoveTo(uint index) return true; } - /// - public void Reset() + public long GetLength() { - fatEnumerator.Reset(); - start = true; - current = Sector.EndOfChain; - Index = uint.MaxValue; + if (length == -1) + { + Reset(); + length = 0; + while (MoveNext()) + { + length++; + } + } + + return length; + } + + /// + /// Extends the chain by one + /// + /// The ID of the new sector + public uint Extend() + { + if (startId == SectorType.EndOfChain) + { + startId = ioContext.Fat.Add(fatEnumerator, 0); + return startId; + } + + uint lastId = startId; + while (MoveNext()) + { + lastId = current.Value; + } + + uint id = ioContext.Fat.Add(fatEnumerator, lastId); + ioContext.Fat[lastId] = id; + return id; } /// - /// Gets the next sector ID in the FAT chain. + /// Returns the ID of the first sector in the chain. /// - uint GetNextFatSectorId(uint id) + public void Extend(uint requiredChainLength) { - if (id > SectorType.Maximum) - throw new ArgumentException("Invalid sector ID", nameof(id)); - - int elementCount = ioContext.Header.SectorSize / sizeof(uint); - uint sectorId = (uint)Math.DivRem(id, elementCount, out long sectorOffset); - if (!fatEnumerator.MoveTo(sectorId)) - throw new ArgumentException("Invalid sector ID", nameof(id)); - - long position = fatEnumerator.Current.Position + sectorOffset * sizeof(uint); - ioContext.Reader.Seek(position); - uint nextId = ioContext.Reader.ReadUInt32(); - return nextId; + uint chainLength = (uint)GetLength(); + if (chainLength >= requiredChainLength) + throw new ArgumentException("The chain is already longer than required.", nameof(requiredChainLength)); + + if (startId == StreamId.NoStream) + { + startId = ioContext.Fat.Add(fatEnumerator, 0); + chainLength = 1; + } + + bool ok = MoveTo(chainLength - 1); + Debug.Assert(ok); + + uint lastId = current.Value; + ok = fatEnumerator.MoveTo(lastId); + Debug.Assert(ok); + while (chainLength < requiredChainLength) + { + uint id = ioContext.Fat.Add(fatEnumerator, lastId); + ioContext.Fat[lastId] = id; + lastId = id; + chainLength++; + } + + this.length = requiredChainLength; + } + + public void Shrink(uint requiredChainLength) + { + uint chainLength = (uint)GetLength(); + if (chainLength <= requiredChainLength) + throw new ArgumentException("The chain is already shorter than required.", nameof(requiredChainLength)); + + Reset(); + + uint lastId = current.Value; + while (MoveNext()) + { + if (lastId is not SectorType.EndOfChain and not SectorType.Free) + { + if (index == requiredChainLength) + ioContext.Fat[lastId] = SectorType.EndOfChain; + else if (index > requiredChainLength) + ioContext.Fat[lastId] = SectorType.Free; + } + + lastId = current.Value; + } + + ioContext.Fat[lastId] = SectorType.Free; + + if (requiredChainLength == 0) + { + startId = StreamId.NoStream; + } + +#if DEBUG + this.length = -1; + this.length = GetLength(); + Debug.Assert(length == requiredChainLength); +#endif + + this.length = requiredChainLength; + } + + /// + public void Reset() => Reset(startId); + + public void Reset(uint startSectorId) + { + startId = startSectorId; + start = true; + index = uint.MaxValue; + current = FatChainEntry.Invalid; } } diff --git a/OpenMcdf3/FatEntry.cs b/OpenMcdf3/FatEntry.cs new file mode 100644 index 00000000..ecef6605 --- /dev/null +++ b/OpenMcdf3/FatEntry.cs @@ -0,0 +1,13 @@ +namespace OpenMcdf3; + +/// +/// Encapsulates an entry in the File Allocation Table (FAT). +/// +internal record struct FatEntry(uint Index, uint Value) +{ + internal static readonly FatEntry Invalid = new(uint.MaxValue, uint.MaxValue); + + public readonly bool IsFree => Value == SectorType.Free; + + public readonly override string ToString() => $"#{Index}: {Value}"; +} diff --git a/OpenMcdf3/FatEnumerator.cs b/OpenMcdf3/FatEnumerator.cs new file mode 100644 index 00000000..d362c6e2 --- /dev/null +++ b/OpenMcdf3/FatEnumerator.cs @@ -0,0 +1,132 @@ +using System.Collections; + +namespace OpenMcdf3; + +/// +/// Enumerates the entries in a FAT. +/// +internal class FatEnumerator : IEnumerator +{ + readonly IOContext ioContext; + bool start = true; + uint index = uint.MaxValue; + uint value = uint.MaxValue; + + public FatEnumerator(IOContext ioContext) + { + this.ioContext = ioContext; + } + + /// + public void Dispose() + { + } + + public Sector CurrentSector + { + get + { + if (index == uint.MaxValue) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return new(index, ioContext.SectorSize); + } + } + + /// + public FatEntry Current + { + get + { + if (index == uint.MaxValue) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return new(index, value); + } + } + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if (start) + { + start = false; + return MoveTo(0); + } + + if (index >= SectorType.Maximum) + return false; + + uint next = index + 1; + return MoveTo(next); + } + + public bool MoveTo(uint index) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(index); + + start = false; + if (this.index == index) + return true; + + if (ioContext.Fat.TryGetValue(index, out value)) + { + this.index = index; + return true; + } + + this.index = uint.MaxValue; + return false; + } + + public bool MoveNextFreeEntry() + { + while (MoveNext()) + { + if (value == SectorType.Free) + return true; + } + + return false; + } + + /// + public void Reset() + { + start = true; + index = uint.MaxValue; + value = uint.MaxValue; + } + + internal void Trace(TextWriter writer) + { + Reset(); + + byte[] data = new byte[ioContext.SectorSize]; + + Stream baseStream = ioContext.Reader.BaseStream; + + writer.WriteLine("Start of FAT ================="); + + while (MoveNext()) + { + FatEntry current = Current; + if (current.IsFree) + { + writer.WriteLine($"{current}"); + } + else + { + baseStream.Position = CurrentSector.Position; + baseStream.ReadExactly(data, 0, data.Length); + string hex = BitConverter.ToString(data); + writer.WriteLine($"{current}: {hex}"); + } + } + + writer.WriteLine("End of FAT ==================="); + } + + public override string ToString() => $"{Current}"; +} diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index f9530a12..9ddf0cb4 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -9,9 +9,8 @@ internal sealed class FatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; private bool start = true; - private uint id = SectorType.EndOfChain; + private uint index = uint.MaxValue; private uint difatSectorId; - private uint difatSectorElementIndex = 0; private Sector current = Sector.EndOfChain; public FatSectorEnumerator(IOContext ioContext) @@ -31,7 +30,7 @@ public Sector Current { get { - if (current.IsEndOfChain) + if (current.Id == SectorType.EndOfChain) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); return current; } @@ -43,42 +42,30 @@ public Sector Current /// public bool MoveNext() { - if (start) - { - id = uint.MaxValue; - start = false; - } - - id++; + start = false; - if (id < ioContext.Header.FatSectorCount && id < Header.DifatArrayLength) + uint nextIndex = index + 1; + if (nextIndex < ioContext.Header.FatSectorCount && nextIndex < Header.DifatArrayLength) // Include the free entries { - uint id = ioContext.Header.Difat[this.id]; - current = new Sector(id, ioContext.Header.SectorSize); + uint id = ioContext.Header.Difat[nextIndex]; + index = nextIndex; + current = new Sector(id, ioContext.SectorSize); return true; } if (difatSectorId == SectorType.EndOfChain) { + index = uint.MaxValue; current = Sector.EndOfChain; - id = SectorType.EndOfChain; return false; } - int difatElementCount = ioContext.Header.SectorSize / sizeof(uint) - 1; - Sector difatSector = new(difatSectorId, ioContext.Header.SectorSize); - long position = difatSector.Position + difatSectorElementIndex * sizeof(uint); - ioContext.Reader.Seek(position); - uint sectorId = ioContext.Reader.ReadUInt32(); - current = new Sector(sectorId, ioContext.Header.SectorSize); - difatSectorElementIndex++; - id++; + Sector difatSector = new(difatSectorId, ioContext.SectorSize); + index = nextIndex; + current = difatSector; - if (difatSectorElementIndex == difatElementCount) - { - difatSectorId = ioContext.Reader.ReadUInt32(); - difatSectorElementIndex = 0; - } + ioContext.Reader.Position = difatSector.EndPosition - sizeof(uint); + difatSectorId = ioContext.Reader.ReadUInt32(); return true; } @@ -86,15 +73,32 @@ public bool MoveNext() /// /// Moves the enumerator to the specified sector. /// - public bool MoveTo(uint sectorId) + public bool MoveTo(uint index) { - if (sectorId > SectorType.Maximum) - throw new ArgumentOutOfRangeException(nameof(sectorId)); + ThrowHelper.ThrowIfSectorIdIsInvalid(index); - if (sectorId < id) - Reset(); + start = false; - while (start || id < sectorId) + if (index == this.index) + return true; + + if (index >= ioContext.Header.FatSectorCount + ioContext.Header.DifatSectorCount) + { + this.index = uint.MaxValue; + current = Sector.EndOfChain; + return false; + } + + if (this.index < Header.DifatArrayLength || index < this.index) + { + // Jump as close as possible + this.index = Math.Min(index, Header.DifatArrayLength - 1); + uint id = ioContext.Header.Difat[this.index]; + current = new(id, ioContext.SectorSize); + difatSectorId = ioContext.Header.FirstDifatSectorId; + } + + while (this.index < index) { if (!MoveNext()) return false; @@ -107,9 +111,74 @@ public bool MoveTo(uint sectorId) public void Reset() { start = true; - id = SectorType.EndOfChain; + index = uint.MaxValue; difatSectorId = ioContext.Header.FirstDifatSectorId; - difatSectorElementIndex = 0; current = Sector.EndOfChain; } + + (uint lastIndex, Sector lastSector) MoveToEnd() + { + Reset(); + + uint lastIndex = uint.MaxValue; + Sector lastSector = Sector.EndOfChain; + while (MoveNext()) + { + lastIndex = index; + lastSector = current; + } + + return (lastIndex, lastSector); + } + + /// + /// Extends the FAT by adding a new sector. + /// + /// The ID of the new sector that was added + public uint Add() + { + // No FAT sectors are free, so add a new one + Header header = ioContext.Header; + (uint lastIndex, Sector lastSector) = MoveToEnd(); + uint nextIndex = lastIndex + 1; + long id = Math.Max(0, (ioContext.Reader.BaseStream.Length - ioContext.SectorSize) / ioContext.SectorSize); // TODO: Check + Sector newSector = new((uint)id, ioContext.SectorSize); + + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = newSector.Position; + writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); + + uint sectorType; + if (nextIndex < Header.DifatArrayLength) + { + index = nextIndex; + current = newSector; + sectorType = SectorType.Fat; + + header.Difat[nextIndex] = newSector.Id; + header.FatSectorCount++; // TODO: Check + } + else + { + index = nextIndex; + current = newSector; + difatSectorId = newSector.Id; + sectorType = SectorType.Difat; + + writer.Position = newSector.EndPosition - sizeof(uint); + writer.Write(SectorType.EndOfChain); + + writer.Position = lastSector.EndPosition - sizeof(uint); + writer.Write(newSector.Id); + + // Chain the sector + if (header.FirstDifatSectorId == SectorType.EndOfChain) + header.FirstDifatSectorId = newSector.Id; + header.DifatSectorCount++; + } + + ioContext.Fat[newSector.Id] = sectorType; + + return newSector.Id; + } } diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 547b52fd..cf8f506e 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -7,7 +7,6 @@ internal class FatStream : Stream { readonly IOContext ioContext; readonly FatChainEnumerator chain; - readonly long length; long position; bool disposed; @@ -15,13 +14,14 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) { this.ioContext = ioContext; DirectoryEntry = directoryEntry; - length = directoryEntry.StreamLength; chain = new(ioContext, directoryEntry.StartSectorId); } /// internal DirectoryEntry DirectoryEntry { get; private set; } + internal long ChainCapacity => ((Length + ioContext.SectorSize - 1) / ioContext.SectorSize) * ioContext.SectorSize; + /// public override bool CanRead => true; @@ -29,10 +29,10 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) public override bool CanSeek => true; /// - public override bool CanWrite => false; + public override bool CanWrite => ioContext.CanWrite; /// - public override long Length => length; + public override long Length => DirectoryEntry.StreamLength; /// public override long Position @@ -54,30 +54,27 @@ protected override void Dispose(bool disposing) } /// - public override void Flush() => this.ThrowIfDisposed(disposed); + public override void Flush() + { + this.ThrowIfDisposed(disposed); + ioContext.Writer!.Flush(); // TODO: Check validity + } /// public override int Read(byte[] buffer, int offset, int count) { - if (buffer is null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number"); - - if ((uint)count > buffer.Length - offset) - 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"); + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); this.ThrowIfDisposed(disposed); if (count == 0) return 0; - int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + int maxCount = (int)Math.Min(Math.Max(Length - position, 0), int.MaxValue); if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; @@ -85,10 +82,10 @@ public override int Read(byte[] buffer, int offset, int count) int readCount = 0; do { - Sector sector = chain.Current; + Sector sector = chain.CurrentSector; int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); - ioContext.Reader.Seek(sector.Position + sectorOffset); + ioContext.Reader.Position = sector.Position + sectorOffset; int localOffset = offset + readCount; int read = ioContext.Reader.Read(buffer, localOffset, (int)readLength); if (read == 0) @@ -112,19 +109,19 @@ public override long Seek(long offset, SeekOrigin origin) { case SeekOrigin.Begin: if (offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position = offset; break; case SeekOrigin.Current: if (position + offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position += offset; break; case SeekOrigin.End: if (Length - offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position = Length - offset; break; @@ -136,8 +133,60 @@ public override long Seek(long offset, SeekOrigin origin) } /// - public override void SetLength(long value) => throw new NotSupportedException(); + public override void SetLength(long value) + { + this.ThrowIfNotWritable(); + + uint requiredChainLength = (uint)((value + ioContext.SectorSize - 1) / ioContext.SectorSize); + if (value > ChainCapacity) + chain.Extend(requiredChainLength); + else if (value <= ChainCapacity - ioContext.SectorSize) + chain.Shrink(requiredChainLength); + + if (DirectoryEntry.StartSectorId != chain.StartId || DirectoryEntry.StreamLength != value) + { + DirectoryEntry.StartSectorId = chain.StartId; + DirectoryEntry.StreamLength = value; + ioContext.Write(DirectoryEntry); + } + } /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + + this.ThrowIfNotWritable(); + + if (count == 0) + return; + + if (position + count > ChainCapacity) + SetLength(position + count); + + uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + throw new InvalidOperationException($"Failed to move to FAT chain index: {chainIndex}"); + + CfbBinaryWriter writer = ioContext.Writer!; + int writeCount = 0; + do + { + Sector sector = chain.CurrentSector; + writer.Position = sector.Position + sectorOffset; + int remaining = count - writeCount; + int localOffset = offset + writeCount; + long writeLength = Math.Min(remaining, sector.Length - sectorOffset); + writer.Write(buffer, localOffset, (int)writeLength); + position += writeLength; + writeCount += (int)writeLength; + if (position > Length) + DirectoryEntry.StreamLength = position; + sectorOffset = 0; + if (writeCount >= count) + return; + } while (chain.MoveNext()); + + throw new InvalidOperationException($"End of FAT chain was reached"); + } } diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 93febf4f..7225028f 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -1,16 +1,17 @@ -namespace OpenMcdf3; + +namespace OpenMcdf3; /// /// The structure at the beginning of a compound file. /// -internal sealed class Header +internal sealed class Header : IEquatable { internal const int DifatArrayLength = 109; internal const ushort ExpectedMinorVersion = 0x003E; internal const ushort LittleEndian = 0xFFFE; internal const ushort SectorShiftV3 = 0x0009; internal const ushort SectorShiftV4 = 0x000C; - internal const short MiniSectorShift = 6; + internal const ushort ExpectedMiniSectorShift = 6; internal const uint MiniStreamCutoffSize = 4096; /// @@ -22,6 +23,7 @@ internal sealed class Header private ushort majorVersion; private ushort sectorShift = SectorShiftV3; + private ushort miniSectorShift = ExpectedMiniSectorShift; /// /// Reserved and unused class ID. @@ -54,16 +56,28 @@ public ushort SectorShift get => sectorShift; set { if (MajorVersion == 3 && value != SectorShiftV3) - throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV3:X4} is supported for Major Version 3"); + throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV3:X4} is supported for Major Version 3."); if (MajorVersion == 4 && value != SectorShiftV4) - throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV4:X4} is supported for Major Version 4"); + throw new FormatException($"Unsupported sector shift {value:X4}. Only {SectorShiftV4:X4} is supported for Major Version 4."); sectorShift = value; } } + public ushort MiniSectorShift + { + get => miniSectorShift; + set + { + if (value != ExpectedMiniSectorShift) + throw new FormatException($"Unsupported sector shift {value:X4}. Only {ExpectedMiniSectorShift:X4} is supported."); + + miniSectorShift = value; + } + } + /// - /// The number of directory sectors in the compound file. + /// The number of directory sectors in the compound file (not used in V3). /// public uint DirectorySectorCount { get; set; } @@ -98,7 +112,7 @@ public ushort SectorShift public uint FirstDifatSectorId { get; set; } = SectorType.EndOfChain; /// - /// The number of DIFACT sectors in the compound file. + /// The number of DIFAT sectors in the compound file. /// public uint DifatSectorCount { get; set; } @@ -107,17 +121,44 @@ public ushort SectorShift /// public uint[] Difat { get; } = new uint[DifatArrayLength]; - /// - /// The size of a regular sector. - /// - public int SectorSize => 1 << SectorShift; - public Header(Version version = Version.V3) { MajorVersion = (ushort)version; + MinorVersion = ExpectedMinorVersion; + SectorShift = version switch + { + Version.V3 => SectorShiftV3, + Version.V4 => SectorShiftV4, + _ => throw new FormatException($"Unsupported version: {version}.") + }; + FirstDirectorySectorId = SectorType.EndOfChain; + DirectorySectorCount = 0; // Not used in v3 + FatSectorCount = 0; for (int i = 0; i < Difat.Length; i++) { Difat[i] = SectorType.Free; } } + + public override bool Equals(object? obj) => Equals(obj as Header); + + public bool Equals(Header? other) + { + return other is not null + && CLSID == other.CLSID + && MinorVersion == other.MinorVersion + && MajorVersion == other.MajorVersion + && SectorShift == other.SectorShift + && DirectorySectorCount == other.DirectorySectorCount + && FatSectorCount == other.FatSectorCount + && FirstDirectorySectorId == other.FirstDirectorySectorId + && TransactionSignature == other.TransactionSignature + && FirstMiniFatSectorId == other.FirstMiniFatSectorId + && MiniFatSectorCount == other.MiniFatSectorCount + && FirstDifatSectorId == other.FirstDifatSectorId + && DifatSectorCount == other.DifatSectorCount + && Difat.SequenceEqual(other.Difat); + } + + public override string ToString() => $"MajorVersion: {MajorVersion}, MinorVersion: {MinorVersion}, FirstDirectorySectorId: {FirstDirectorySectorId}, FirstMiniFatSectorId: {FirstMiniFatSectorId}"; } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index c9619a00..817dc110 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -12,46 +12,123 @@ enum IOContextFlags /// internal sealed class IOContext : IDisposable { + readonly DirectoryEntryEnumerator directoryEnumerator; + readonly CfbBinaryWriter? writer; + MiniFat? miniFat; + FatStream? miniStream; + public Header Header { get; } public CfbBinaryReader Reader { get; } - public CfbBinaryWriter? Writer { get; } + public CfbBinaryWriter Writer + { + get + { + if (writer is null) + throw new InvalidOperationException("Stream is not writable"); + return writer; + } + } + + public Fat Fat { get; } public DirectoryEntry RootEntry { get; } + public MiniFat MiniFat + { + get + { + miniFat ??= new(this); + return miniFat; + } + } + + public FatStream MiniStream + { + get + { + miniStream ??= new(this, RootEntry); + return miniStream; + } + } + + public bool CanWrite => writer is not null; + public bool IsDisposed { get; private set; } + /// + /// The size of a regular sector. + /// + public int SectorSize { get; } + + public int MiniSectorSize { get; } + + public Version Version => (Version)Header.MajorVersion; + public IOContext(Header header, CfbBinaryReader reader, CfbBinaryWriter? writer, IOContextFlags contextFlags = IOContextFlags.None) { + if (contextFlags.HasFlag(IOContextFlags.Create) && writer is null) + throw new ArgumentNullException(nameof(writer), "A writer is required to create a new compound file."); + Header = header; Reader = reader; - Writer = writer; - RootEntry = contextFlags.HasFlag(IOContextFlags.Create) - ? new DirectoryEntry() - : EnumerateDirectoryEntries().First(); - // TODO: Improve root directory entry validation + this.writer = writer; + + SectorSize = 1 << header.SectorShift; + MiniSectorSize = 1 << header.MiniSectorShift; + + Fat = new(this); + directoryEnumerator = new(this); + + if (contextFlags.HasFlag(IOContextFlags.Create)) + { + RootEntry = directoryEnumerator.CreateOrRecycleDirectoryEntry(); + RootEntry.RecycleRoot(); + + WriteHeader(); + Write(RootEntry); + } + else + { + if (!directoryEnumerator.MoveNext()) + throw new FormatException("Root directory entry not found."); + RootEntry = directoryEnumerator.Current; + } } public void Dispose() { if (!IsDisposed) { + if (CanWrite) + WriteHeader(); + miniStream?.Dispose(); + miniFat?.Dispose(); + directoryEnumerator.Dispose(); + Fat.Dispose(); + writer?.Dispose(); Reader.Dispose(); - Writer?.Dispose(); IsDisposed = true; } } - /// - /// Enumerates all the instances in the compound file. - /// - public IEnumerable EnumerateDirectoryEntries() + public void ExtendStreamLength(long length) { - this.ThrowIfDisposed(IsDisposed); + Stream baseStream = Writer.BaseStream; + if (baseStream.Length < length) + baseStream.SetLength(length); + } - using DirectoryEntryEnumerator directoryEntriesEnumerator = new(this); - while (directoryEntriesEnumerator.MoveNext()) - yield return directoryEntriesEnumerator.Current; + public void WriteHeader() + { + CfbBinaryWriter writer = Writer; + writer.Seek(0, SeekOrigin.Begin); + writer.Write(Header); + } + + public void Write(DirectoryEntry entry) + { + directoryEnumerator.Write(entry); } } diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs new file mode 100644 index 00000000..bab51843 --- /dev/null +++ b/OpenMcdf3/MiniFat.cs @@ -0,0 +1,106 @@ +using System.Collections; +using System.Diagnostics; + +namespace OpenMcdf3; + +/// +/// Encapsulates getting and setting entries in the mini FAT. +/// +internal sealed class MiniFat : IEnumerable, IDisposable +{ + private readonly IOContext ioContext; + private readonly FatChainEnumerator fatChainEnumerator; + + internal int ElementsPerSector => ioContext.SectorSize / sizeof(uint); + + public MiniFat(IOContext ioContext) + { + this.ioContext = ioContext; + fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); + } + + public void Dispose() => fatChainEnumerator.Dispose(); + + public IEnumerator GetEnumerator() => new MiniFatEnumerator(ioContext); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public uint this[uint key] + { + get + { + if (!TryGetValue(key, out uint value)) + throw new KeyNotFoundException($"Mini FAT index not found: {key}."); + return value; + } + set + { + ThrowHelper.ThrowIfSectorIdIsInvalid(key); + + uint fatSectorIndex = (uint)Math.DivRem(key, ElementsPerSector, out long elementIndex); + if (!fatChainEnumerator.MoveTo(fatSectorIndex)) + throw new KeyNotFoundException($"Mini FAT index not found: {fatSectorIndex}."); + + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatChainEnumerator.CurrentSector.Position + elementIndex * sizeof(uint); + writer.Write(value); + } + } + + public bool TryGetValue(uint key, out uint value) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(key); + + uint fatSectorIndex = (uint)Math.DivRem(key, ElementsPerSector, out long elementIndex); + bool ok = fatChainEnumerator.MoveTo(fatSectorIndex); + if (!ok) + { + value = uint.MaxValue; + return false; + } + + CfbBinaryReader reader = ioContext.Reader; + reader.Position = fatChainEnumerator.CurrentSector.Position + elementIndex * sizeof(uint); + value = reader.ReadUInt32(); + return true; + } + + public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(startIndex); + + bool movedToFreeEntry = miniFatEnumerator.MoveTo(startIndex) && miniFatEnumerator.MoveNextFreeEntry(); + if (!movedToFreeEntry) + { + uint newSectorIndex = fatChainEnumerator.Extend(); + Sector sector = new(newSectorIndex, ioContext.SectorSize); + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = sector.Position; + writer.Write(SectorDataCache.GetFatEntryData(sector.Length)); + + if (ioContext.Header.FirstMiniFatSectorId == SectorType.EndOfChain) + ioContext.Header.FirstMiniFatSectorId = newSectorIndex; + + miniFatEnumerator.Reset(); // TODO: Jump closer to the new sector + + bool ok = miniFatEnumerator.MoveNextFreeEntry(); + Debug.Assert(ok, "No free mini FAT entries found."); + } + + FatEntry entry = miniFatEnumerator.Current; + this[entry.Index] = SectorType.EndOfChain; + + Debug.Assert(entry.IsFree); + MiniSector miniSector = new(entry.Index, ioContext.MiniSectorSize); + if (ioContext.MiniStream.Length < miniSector.EndPosition) + ioContext.MiniStream.SetLength(miniSector.EndPosition); + + return entry.Index; + } + + internal void Trace(TextWriter writer) + { + using MiniFatEnumerator miniFatEnumerator = new(ioContext); + miniFatEnumerator.Trace(writer); + } +} diff --git a/OpenMcdf3/MiniFatChainEnumerator.cs b/OpenMcdf3/MiniFatChainEnumerator.cs index a5a8e407..a78be00f 100644 --- a/OpenMcdf3/MiniFatChainEnumerator.cs +++ b/OpenMcdf3/MiniFatChainEnumerator.cs @@ -1,19 +1,24 @@ using System.Collections; +using System.Diagnostics; namespace OpenMcdf3; /// /// Enumerates the s in a Mini FAT sector chain. /// -internal sealed class MiniFatChainEnumerator : IEnumerator +internal sealed class MiniFatChainEnumerator : IEnumerator { + private readonly IOContext ioContext; private readonly MiniFatEnumerator miniFatEnumerator; - private readonly uint startId; + private uint startId; private bool start = true; - private MiniSector current = MiniSector.EndOfChain; + uint index = uint.MaxValue; + private FatChainEntry current = FatChainEntry.Invalid; + private long length = -1; public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) { + this.ioContext = ioContext; this.startId = startSectorId; miniFatEnumerator = new(ioContext); } @@ -21,20 +26,24 @@ public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) /// public void Dispose() { - miniFatEnumerator.Dispose(); } /// /// The index within the Mini FAT sector chain, or if the enumeration has not started. /// - public uint Index { get; private set; } = uint.MaxValue; + + public uint StartId => startId; + + public uint Index => index; + + public MiniSector CurrentSector => new(Current.Value, ioContext.MiniSectorSize); /// - public MiniSector Current + public FatChainEntry Current { get { - if (current.IsEndOfChain) + if (current.IsFreeOrEndOfChain) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); return current; } @@ -48,21 +57,33 @@ public bool MoveNext() { if (start) { - current = new(startId); - Index = 0; start = false; + index = 0; + current = new(index, startId); } - else if (!current.IsEndOfChain) + else if (!current.IsFreeOrEndOfChain) { - uint sectorId = GetNextMiniFatSectorId(current.Id); - current = new(sectorId); - Index++; + uint sectorId = ioContext.MiniFat[current.Value]; + if (sectorId == SectorType.EndOfChain) + { + index = uint.MaxValue; + current = FatChainEntry.Invalid; + return false; + } + + uint nextIndex = index + 1; + if (nextIndex > SectorType.Maximum) + throw new FormatException("Mini FAT chain is corrupt."); + + index = nextIndex; + current = new(nextIndex, sectorId); + return true; } - if (current.IsEndOfChain) + if (current.IsFreeOrEndOfChain) { - current = MiniSector.EndOfChain; - Index = uint.MaxValue; + index = uint.MaxValue; + current = FatChainEntry.Invalid; return false; } @@ -76,10 +97,10 @@ public bool MoveNext() /// true if the enumerator was successfully advanced to the given index public bool MoveTo(uint index) { - if (index < Index) + if (index < this.index) Reset(); - while (start || Index < index) + while (start || this.index < index) { if (!MoveNext()) return false; @@ -88,21 +109,102 @@ public bool MoveTo(uint index) return true; } + public long GetLength() + { + if (length == -1) + { + Reset(); + length = 0; + while (MoveNext()) + { + length++; + } + } + + return length; + } + + public void Extend(uint requiredChainLength) + { + uint chainLength = (uint)GetLength(); + if (chainLength >= requiredChainLength) + throw new ArgumentException("The chain is already longer than required.", nameof(requiredChainLength)); + + if (startId == StreamId.NoStream) + { + startId = ioContext.MiniFat.Add(miniFatEnumerator, 0); + chainLength = 1; + } + + bool ok = MoveTo(chainLength - 1); + Debug.Assert(ok); + + uint lastId = current.Value; + ok = miniFatEnumerator.MoveTo(lastId); + Debug.Assert(ok); + while (chainLength < requiredChainLength) + { + uint id = ioContext.MiniFat.Add(miniFatEnumerator, lastId); + ioContext.MiniFat[lastId] = id; + lastId = id; + chainLength++; + } + +#if DEBUG + this.length = -1; + this.length = GetLength(); + Debug.Assert(length == requiredChainLength); +#endif + + this.length = requiredChainLength; + } + + public void Shrink(uint requiredChainLength) + { + uint chainLength = (uint)GetLength(); + if (chainLength <= requiredChainLength) + throw new ArgumentException("The chain is already shorter than required.", nameof(requiredChainLength)); + + Reset(); + + uint lastId = current.Value; + while (MoveNext()) + { + if (lastId <= SectorType.Maximum) + { + if (index == requiredChainLength) + ioContext.MiniFat[lastId] = SectorType.EndOfChain; + else if (index > requiredChainLength) + ioContext.MiniFat[lastId] = SectorType.Free; + } + + lastId = current.Value; + } + + if (lastId <= SectorType.Maximum) + ioContext.MiniFat[lastId] = SectorType.Free; + + if (requiredChainLength == 0) + { + startId = StreamId.NoStream; + } + +#if DEBUG + this.length = -1; + this.length = GetLength(); + Debug.Assert(length == requiredChainLength); +#endif + + this.length = requiredChainLength; + } + /// public void Reset() { start = true; - miniFatEnumerator.Reset(); - current = MiniSector.EndOfChain; - Index = uint.MaxValue; + index = uint.MaxValue; + current = FatChainEntry.Invalid; } - /// - /// Gets the next sector ID in the FAT chain. - /// - uint GetNextMiniFatSectorId(uint id) - { - miniFatEnumerator.MoveTo(id); - return miniFatEnumerator.Current.Id; - } + public override string ToString() => $"Index: {index} Value {current}"; } diff --git a/OpenMcdf3/MiniFatEnumerator.cs b/OpenMcdf3/MiniFatEnumerator.cs index d1aa852c..48c9e017 100644 --- a/OpenMcdf3/MiniFatEnumerator.cs +++ b/OpenMcdf3/MiniFatEnumerator.cs @@ -5,28 +5,44 @@ namespace OpenMcdf3; /// /// Enumerates the s from the Mini FAT. /// -internal sealed class MiniFatEnumerator : IEnumerator +internal sealed class MiniFatEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly MiniFatSectorEnumerator miniFatSectorEnumerator; + private readonly FatChainEnumerator fatChainEnumerator; private bool start = true; - private int index = int.MaxValue; - private MiniSector current = MiniSector.EndOfChain; + private uint index = uint.MaxValue; + private uint value = uint.MaxValue; public MiniFatEnumerator(IOContext ioContext) { - miniFatSectorEnumerator = new(ioContext); + fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); this.ioContext = ioContext; } /// - public MiniSector Current + public void Dispose() + { + fatChainEnumerator.Dispose(); + } + + public MiniSector CurrentSector + { + get + { + if (index == uint.MaxValue) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return new(value, ioContext.MiniSectorSize); + } + } + + /// + public FatEntry Current { get { - if (index == int.MaxValue) + if (index == uint.MaxValue) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); - return current; + return new(index, value); } } @@ -38,65 +54,63 @@ public bool MoveNext() { if (start) { - if (!miniFatSectorEnumerator.MoveNext()) - { - index = int.MaxValue; - return false; - } - - index = -1; start = false; + return MoveTo(0); } - index++; - int elementCount = MiniSector.Length / sizeof(uint); - if (index > elementCount) - { - if (!miniFatSectorEnumerator.MoveNext()) - { - index = int.MaxValue; - return false; - } + if (index >= SectorType.Maximum) + return false; + + uint next = index + 1; + return MoveTo(next); + } - index = 0; + public bool MoveTo(uint index) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(index); + + if (this.index == index) + return true; + + if (ioContext.MiniFat.TryGetValue(index, out value)) + { + this.index = index; + return true; } - long position = miniFatSectorEnumerator.Current.Position + index * sizeof(uint); - ioContext.Reader.Seek(position); - uint sectorId = ioContext.Reader.ReadUInt32(); - current = new(sectorId); - return true; + this.index = uint.MaxValue; + return false; } - public void MoveTo(uint id) + public bool MoveNextFreeEntry() { - if (id > SectorType.Maximum) - throw new ArgumentException("Invalid sector ID", nameof(id)); - - int elementCount = ioContext.Header.SectorSize / sizeof(uint); - uint sectorId = (uint)Math.DivRem(id, elementCount, out long index); - - miniFatSectorEnumerator.MoveTo(sectorId); - long position = miniFatSectorEnumerator.Current.Position + index * sizeof(uint); - ioContext.Reader.Seek(position); - uint value = ioContext.Reader.ReadUInt32(); - this.index = (int)index; - start = false; - current = new(value); + while (MoveNext()) + { + if (value == SectorType.Free) + { + return true; + } + } + + return false; } /// public void Reset() { - miniFatSectorEnumerator.Reset(); + fatChainEnumerator.Reset(ioContext.Header.FirstMiniFatSectorId); start = true; - current = MiniSector.EndOfChain; - index = int.MaxValue; + index = uint.MaxValue; + value = uint.MaxValue; } - /// - public void Dispose() + internal void Trace(TextWriter writer) { - miniFatSectorEnumerator.Dispose(); + Reset(); + + writer.WriteLine("Start of Mini FAT ============"); + while (MoveNext()) + writer.WriteLine($"Mini FAT entry {Current}"); + writer.WriteLine("End of Mini FAT =============="); } } diff --git a/OpenMcdf3/MiniFatSectorEnumerator.cs b/OpenMcdf3/MiniFatSectorEnumerator.cs deleted file mode 100644 index f9552d42..00000000 --- a/OpenMcdf3/MiniFatSectorEnumerator.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections; - -namespace OpenMcdf3; - -/// -/// Enumerates the s in a FAT sector chain. -/// -internal sealed class MiniFatSectorEnumerator : IEnumerator -{ - private readonly IOContext ioContext; - private readonly FatChainEnumerator fatChain; - bool start = true; - MiniSector current = MiniSector.EndOfChain; - - public MiniFatSectorEnumerator(IOContext ioContext) - { - this.ioContext = ioContext; - fatChain = new(ioContext, ioContext.Header.FirstMiniFatSectorId); - } - - /// - public void Dispose() - { - fatChain.Dispose(); - } - - /// - public MiniSector Current - { - get - { - if (current.IsEndOfChain) - throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); - return current; - } - } - - /// - object IEnumerator.Current => Current; - - /// - public bool MoveNext() - { - if (start) - { - current = new(ioContext.Header.FirstMiniFatSectorId); - start = false; - } - else if (!current.IsEndOfChain) - { - uint sectorId = GetNextMiniFatSectorId(current.Id); - current = new(sectorId); - } - - return !current.IsEndOfChain; - } - - /// - /// Moves the enumerator to the specified sector. - /// - public bool MoveTo(uint sectorId) - { - if (sectorId > SectorType.Maximum) - throw new ArgumentOutOfRangeException(nameof(sectorId)); - - if (sectorId < current.Id) - Reset(); - - while (start || current.Id < sectorId) - { - if (!MoveNext()) - return false; - } - - return true; - } - - /// - public void Reset() - { - start = true; - current = MiniSector.EndOfChain; - } - - /// - /// Gets the next mini FAT sector ID. - /// - /// - /// - /// - public uint GetNextMiniFatSectorId(uint sectorId) - { - if (sectorId > SectorType.Maximum) - throw new ArgumentException($"Invalid sector ID: {sectorId}", nameof(sectorId)); - - int elementLength = ioContext.Header.SectorSize / sizeof(uint); - uint fatSectorId = (uint)Math.DivRem(sectorId, elementLength, out long sectorOffset); - if (!fatChain.MoveTo(fatSectorId)) - throw new ArgumentException($"Invalid sector ID: {sectorId}", nameof(sectorId)); - - long position = fatChain.Current.Position + sectorOffset * sizeof(uint); - ioContext.Reader.Seek(position); - uint nextId = ioContext.Reader.ReadUInt32(); - return nextId; - } -} diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index 915d5e88..bd274f32 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -6,9 +6,7 @@ internal sealed class MiniFatStream : Stream { readonly IOContext ioContext; - readonly MiniFatChainEnumerator chain; - readonly FatStream fatStream; - readonly long length; + readonly MiniFatChainEnumerator miniChain; long position; bool disposed; @@ -16,20 +14,20 @@ internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) { this.ioContext = ioContext; DirectoryEntry = directoryEntry; - length = directoryEntry.StreamLength; - chain = new(ioContext, directoryEntry.StartSectorId); - fatStream = new(ioContext, ioContext.RootEntry); + miniChain = new(ioContext, directoryEntry.StartSectorId); } internal DirectoryEntry DirectoryEntry { get; private set; } + internal long ChainCapacity => ((Length + ioContext.MiniSectorSize - 1) / ioContext.MiniSectorSize) * ioContext.MiniSectorSize; + public override bool CanRead => true; public override bool CanSeek => true; - public override bool CanWrite => false; + public override bool CanWrite => ioContext.CanWrite; - public override long Length => length; + public override long Length => DirectoryEntry.StreamLength; public override long Position { @@ -41,8 +39,7 @@ protected override void Dispose(bool disposing) { if (!disposed) { - chain.Dispose(); - fatStream.Dispose(); + miniChain.Dispose(); disposed = true; } @@ -52,43 +49,38 @@ protected override void Dispose(bool disposing) public override void Flush() { this.ThrowIfDisposed(disposed); - fatStream.Flush(); + + ioContext.MiniStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) { - if (buffer is null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number"); - - if ((uint)count > buffer.Length - offset) - 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"); + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); this.ThrowIfDisposed(disposed); if (count == 0) return 0; - int maxCount = (int)Math.Min(Math.Max(length - position, 0), int.MaxValue); + int maxCount = (int)Math.Min(Math.Max(Length - position, 0), int.MaxValue); if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.Header.SectorSize, out long sectorOffset); - if (!chain.MoveTo(chainIndex)) + uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + if (!miniChain.MoveTo(chainIndex)) return 0; + FatStream miniStream = ioContext.MiniStream; int realCount = Math.Min(count, maxCount); int readCount = 0; do { - MiniSector sector = chain.Current; + MiniSector miniSector = miniChain.CurrentSector; int remaining = realCount - readCount; - long readLength = Math.Min(remaining, buffer.Length); - fatStream.Position = sector.Position + sectorOffset; + long readLength = Math.Min(remaining, miniSector.Length - sectorOffset); + miniStream.Position = miniSector.Position + sectorOffset; int localOffset = offset + readCount; - int read = fatStream.Read(buffer, localOffset, (int)readLength); + int read = miniStream.Read(buffer, localOffset, (int)readLength); if (read == 0) return readCount; position += read; @@ -96,7 +88,7 @@ public override int Read(byte[] buffer, int offset, int count) sectorOffset = 0; if (readCount >= realCount) return readCount; - } while (chain.MoveNext()); + } while (miniChain.MoveNext()); return readCount; } @@ -109,30 +101,88 @@ public override long Seek(long offset, SeekOrigin origin) { case SeekOrigin.Begin: if (offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position = offset; break; case SeekOrigin.Current: if (position + offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position += offset; break; case SeekOrigin.End: if (Length - offset < 0) - throw new IOException("Seek before origin"); + ThrowHelper.ThrowSeekBeforeOrigin(); position = Length - offset; break; default: - throw new ArgumentException(nameof(origin), "Invalid seek origin"); + throw new ArgumentException(nameof(origin), "Invalid seek origin."); } return position; } - public override void SetLength(long value) => throw new NotSupportedException(); + public override void SetLength(long value) + { + if (value >= Header.MiniStreamCutoffSize) + throw new ArgumentOutOfRangeException(nameof(value)); + + this.ThrowIfDisposed(disposed); + this.ThrowIfNotWritable(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + uint requiredChainLength = (uint)((value + ioContext.MiniSectorSize - 1) / ioContext.MiniSectorSize); + if (value > ChainCapacity) + miniChain.Extend(requiredChainLength); + else if (value <= ChainCapacity - ioContext.MiniSectorSize) + miniChain.Shrink(requiredChainLength); + + if (DirectoryEntry.StartSectorId != miniChain.StartId || DirectoryEntry.StreamLength != value) + { + DirectoryEntry.StartSectorId = miniChain.StartId; + DirectoryEntry.StreamLength = value; + ioContext.Write(DirectoryEntry); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + + this.ThrowIfDisposed(disposed); + this.ThrowIfNotWritable(); + + if (count == 0) + return; + + if (position + count > ChainCapacity) + SetLength(position + count); + + uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + if (!miniChain.MoveTo(chainIndex)) + throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); + + FatStream miniStream = ioContext.MiniStream; + int writeCount = 0; + do + { + MiniSector miniSector = miniChain.CurrentSector; + long basePosition = miniSector.Position + sectorOffset; + miniStream.Seek(basePosition, SeekOrigin.Begin); + int remaining = count - writeCount; + int localOffset = offset + writeCount; + long writeLength = Math.Min(remaining, ioContext.MiniSectorSize - sectorOffset); + miniStream.Write(buffer, localOffset, (int)writeLength); + position += writeLength; + writeCount += (int)writeLength; + if (position > Length) + DirectoryEntry.StreamLength = position; + sectorOffset = 0; + if (writeCount >= count) + return; + } while (miniChain.MoveNext()); + + throw new InvalidOperationException($"End of mini FAT chain was reached."); + } } diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs index e30b6e81..a8d0e105 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf3/MiniSector.cs @@ -4,11 +4,9 @@ /// Encapsulates information about a mini sector in a compound file. /// /// The ID of the mini sector -internal record struct MiniSector(uint Id) +internal record struct MiniSector(uint Id, int Length) { - public const int Length = 64; - - public static readonly MiniSector EndOfChain = new(SectorType.EndOfChain); + public static readonly MiniSector EndOfChain = new(SectorType.EndOfChain, int.MaxValue); public readonly bool IsValid => Id <= SectorType.Maximum; @@ -41,7 +39,7 @@ public readonly long EndPosition readonly void ThrowIfInvalid() { if (!IsValid) - throw new InvalidOperationException($"Invalid sector ID: {Id}"); + throw new InvalidOperationException($"Invalid mini FAT sector ID: {Id}."); } public override readonly string ToString() => $"{Id}"; diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj index 1ace06ea..081a0aef 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -1,11 +1,15 @@  - netstandard2.0 + netstandard2.0;netstandard2.1;net8.0 11.0 enable enable true + + + + diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 3df2d7ff..6ce63152 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -14,7 +14,13 @@ public sealed class RootStorage : Storage, IDisposable public static RootStorage Create(string fileName, Version version = Version.V3) { FileStream stream = File.Create(fileName); + return Create(stream, version); + } + + public static RootStorage Create(Stream stream, Version version = Version.V3) + { Header header = new(version); + stream.SetLength(0); CfbBinaryReader reader = new(stream); CfbBinaryWriter writer = new(stream); IOContext ioContext = new(header, reader, writer, IOContextFlags.Create); @@ -35,9 +41,16 @@ public static RootStorage OpenRead(string fileName) public static RootStorage Open(Stream stream, bool leaveOpen = false) { + stream.Position = 0; + + Header header; + using (CfbBinaryReader headerReader = new(stream)) + { + header = headerReader.ReadHeader(); + } + CfbBinaryReader reader = new(stream); CfbBinaryWriter? writer = stream.CanWrite ? new(stream) : null; - Header header = reader.ReadHeader(); IOContextFlags contextFlags = leaveOpen ? IOContextFlags.LeaveOpen : IOContextFlags.None; IOContext ioContext = new(header, reader, writer, contextFlags); return new RootStorage(ioContext); @@ -49,4 +62,11 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) } public void Dispose() => ioContext?.Dispose(); + + internal void Trace(TextWriter writer) + { + writer.WriteLine(ioContext.Header); + ioContext.Fat.Trace(writer); + ioContext.MiniFat.Trace(writer); + } } diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf3/Sector.cs index 94a9ae9a..07beb12d 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf3/Sector.cs @@ -18,7 +18,7 @@ internal record struct Sector(uint Id, int Length) public readonly bool IsValid => Id <= SectorType.Maximum; /// - /// The position of the mini sector in the compound file stream. + /// The position of the sector in the compound file stream. /// public readonly long Position { @@ -30,7 +30,7 @@ public readonly long Position } /// - /// The end position of the mini sector in the compound file stream. + /// The end position of the sector in the compound file stream. /// public readonly long EndPosition { @@ -44,7 +44,7 @@ public readonly long EndPosition readonly void ThrowIfInvalid() { if (!IsValid) - throw new InvalidOperationException($"Invalid sector ID: {Id}"); + throw new InvalidOperationException($"Invalid FAT sector ID: {Id}."); } public override readonly string ToString() => $"{Id}"; diff --git a/OpenMcdf3/SectorDataCache.cs b/OpenMcdf3/SectorDataCache.cs new file mode 100644 index 00000000..77085b56 --- /dev/null +++ b/OpenMcdf3/SectorDataCache.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; +using System.Runtime.InteropServices; + +namespace OpenMcdf3; + +/// +/// Caches data for adding new sectors to the FAT. +/// +internal static class SectorDataCache +{ + static readonly ConcurrentDictionary freeFatSectorData = new(1, 2); + + public static byte[] GetFatEntryData(int sectorSize) + { + if (!freeFatSectorData.TryGetValue(sectorSize, out byte[]? data)) + { + data = new byte[sectorSize]; + Span uintSpan = MemoryMarshal.Cast(data); + uintSpan.Fill(SectorType.Free); + freeFatSectorData.TryAdd(sectorSize, data); + } + + return data; + } +} diff --git a/OpenMcdf3/SectorType.cs b/OpenMcdf3/SectorType.cs index 83098373..83582262 100644 --- a/OpenMcdf3/SectorType.cs +++ b/OpenMcdf3/SectorType.cs @@ -10,4 +10,6 @@ internal static class SectorType public const uint Fat = 0xFFFFFFFD; public const uint EndOfChain = 0xFFFFFFFE; public const uint Free = 0xFFFFFFFF; + + public static bool IsFreeOrEndOfChain(uint value) => value is Free or EndOfChain; } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index ac428018..b3b3a347 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -17,7 +17,7 @@ internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) public IEnumerable EnumerateEntries() { - this.ThrowIfDisposed(ioContext); + this.ThrowIfDisposed(ioContext.IsDisposed); return EnumerateDirectoryEntries() .Select(e => e.ToEntryInfo()); @@ -25,7 +25,7 @@ public IEnumerable EnumerateEntries() public IEnumerable EnumerateEntries(StorageType type) { - this.ThrowIfDisposed(ioContext); + this.ThrowIfDisposed(ioContext.IsDisposed); return EnumerateDirectoryEntries(type) .Select(e => e.ToEntryInfo()); @@ -33,8 +33,6 @@ public IEnumerable EnumerateEntries(StorageType type) IEnumerable EnumerateDirectoryEntries() { - this.ThrowIfDisposed(ioContext); - using DirectoryTreeEnumerator treeEnumerator = new(ioContext, DirectoryEntry); while (treeEnumerator.MoveNext()) { @@ -45,25 +43,60 @@ IEnumerable EnumerateDirectoryEntries() IEnumerable EnumerateDirectoryEntries(StorageType type) => EnumerateDirectoryEntries() .Where(e => e.Type == type); + DirectoryEntry? TryGetDirectoryEntry(StorageType storageType, string name) + { + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + return directoryTreeEnumerator.TryGetDirectoryEntry(storageType, name); + } + + DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) + { + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + return directoryTreeEnumerator.Add(storageType, name); + } + + public Storage CreateStorage(string name) + { + ThrowHelper.ThrowIfNameIsInvalid(name); + + this.ThrowIfDisposed(ioContext.IsDisposed); + + DirectoryEntry entry = AddDirectoryEntry(StorageType.Storage, name); + return new Storage(ioContext, entry); + } + + public CfbStream CreateStream(string name) + { + ThrowHelper.ThrowIfNameIsInvalid(name); + + this.ThrowIfDisposed(ioContext.IsDisposed); + + // TODO: Return a Stream that can transition between FAT and mini FAT + DirectoryEntry entry = AddDirectoryEntry(StorageType.Stream, name); + return new CfbStream(ioContext, entry); + } + public Storage OpenStorage(string name) { - this.ThrowIfDisposed(ioContext); + ThrowHelper.ThrowIfNameIsInvalid(name); + + this.ThrowIfDisposed(ioContext.IsDisposed); - DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Storage) - .FirstOrDefault(entry => entry.Name == name) ?? throw new DirectoryNotFoundException($"Storage not found: {name}"); + DirectoryEntry entry = TryGetDirectoryEntry(StorageType.Storage, name) + ?? throw new DirectoryNotFoundException($"Storage not found: {name}."); return new Storage(ioContext, entry); } - public Stream OpenStream(string name) + public CfbStream OpenStream(string name) { - this.ThrowIfDisposed(ioContext); + ThrowHelper.ThrowIfNameIsInvalid(name); - DirectoryEntry? entry = EnumerateDirectoryEntries(StorageType.Stream) - .FirstOrDefault(entry => entry.Name == name) ?? throw new FileNotFoundException($"Stream not found: {name}", name); - return entry.StreamLength switch - { - < Header.MiniStreamCutoffSize => new MiniFatStream(ioContext, entry), - _ => new FatStream(ioContext, entry) - }; + this.ThrowIfDisposed(ioContext.IsDisposed); + + DirectoryEntry? entry = TryGetDirectoryEntry(StorageType.Stream, name) + ?? throw new FileNotFoundException($"Stream not found: {name}.", name); + + // TODO: Return a Stream that can transition between FAT and mini FAT + return new CfbStream(ioContext, entry); } } diff --git a/OpenMcdf3/StreamExtensions.cs b/OpenMcdf3/StreamExtensions.cs new file mode 100644 index 00000000..b2fd63c4 --- /dev/null +++ b/OpenMcdf3/StreamExtensions.cs @@ -0,0 +1,40 @@ +using System.Buffers; + +namespace OpenMcdf3; + +#if !NET7_0_OR_GREATER + +internal static class StreamExtensions +{ + public static void ReadExactly(this Stream stream, Span buffer) + { + byte[] array = ArrayPool.Shared.Rent(buffer.Length); + try + { + stream.ReadExactly(array, 0, buffer.Length); + array.AsSpan(0, buffer.Length).CopyTo(buffer); + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + public static void ReadExactly(this Stream stream, byte[] buffer, int offset, int count) + { + if (count == 0) + return; + + int totalRead = 0; + do + { + int read = stream.Read(buffer, offset + totalRead, count - totalRead); + if (read == 0) + throw new EndOfStreamException(); + + totalRead += read; + } while (totalRead < count); + } +} + +#endif diff --git a/OpenMcdf3/ThrowHelper.cs b/OpenMcdf3/ThrowHelper.cs index aed8877a..b7c61608 100644 --- a/OpenMcdf3/ThrowHelper.cs +++ b/OpenMcdf3/ThrowHelper.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf3; +using System.Text; + +namespace OpenMcdf3; /// /// Extensions to consistently throw exceptions. @@ -11,9 +13,41 @@ public static void ThrowIfDisposed(this object instance, bool disposed) throw new ObjectDisposedException(instance.GetType().FullName); } - public static void ThrowIfDisposed(this object instance, IOContext context) + public static void ThrowIfStreamArgumentsAreInvalid(byte[] buffer, int offset, int count) + { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be a non-negative number."); + + if ((uint)count > buffer.Length - offset) + 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."); + } + + public static void ThrowIfNotWritable(this Stream stream) + { + if (!stream.CanWrite) + throw new NotSupportedException("Stream does not support writing."); + } + + public static void ThrowSeekBeforeOrigin() => throw new IOException("Seek before origin."); + + public static void ThrowIfNameIsInvalid(string value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + if (value.Contains(@"\") || value.Contains(@"/") || value.Contains(@":") || value.Contains(@"!")) + throw new ArgumentException("Name cannot contain any of the following characters: '\\', '/', ':', '!'.", nameof(value)); + + if (Encoding.Unicode.GetByteCount(value) > DirectoryEntry.NameFieldLength - 2) + throw new ArgumentException($"{value} exceeds maximum encoded length of {DirectoryEntry.NameFieldLength} bytes.", nameof(value)); + } + + public static void ThrowIfSectorIdIsInvalid(uint value) { - if (context.IsDisposed) - throw new InvalidOperationException("Root storage has been disposed"); + if (value > SectorType.Maximum) + throw new ArgumentOutOfRangeException(nameof(value), $"Invalid sector ID: {value:X8}."); } } diff --git a/global.json b/global.json new file mode 100644 index 00000000..1617b795 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.402", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file From c14584e2d2e908f3debe7ac93da0baa957b8634e Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 4 Nov 2024 14:56:38 +1300 Subject: [PATCH 038/114] Apply minor refactor --- OpenMcdf3.Tests/StreamTests.cs | 2 +- OpenMcdf3/CfbBinaryReader.cs | 4 ++-- OpenMcdf3/DirectoryEntryEnumerator.cs | 8 ++++---- OpenMcdf3/DirectoryTreeEnumerator.cs | 2 -- OpenMcdf3/Fat.cs | 6 +++--- OpenMcdf3/FatChainEnumerator.cs | 6 +++--- OpenMcdf3/FatSectorEnumerator.cs | 8 ++++++-- OpenMcdf3/MiniFat.cs | 4 ++-- OpenMcdf3/MiniFatChainEnumerator.cs | 6 +++--- OpenMcdf3/MiniFatStream.cs | 2 +- OpenMcdf3/MiniSector.cs | 1 + OpenMcdf3/RootStorage.cs | 2 +- 12 files changed, 27 insertions(+), 24 deletions(-) diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 5be7f8b3..d0249f72 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -252,7 +252,7 @@ public void WriteMultiple(Version version, int length) [DataRow(Version.V3, 0)] [DataRow(Version.V3, 63)] [DataRow(Version.V3, 64)] // Mini-stream sector size - [DataRow(Version.V3, 2* 64)] // Simplest case (1 sector => 2) + [DataRow(Version.V3, 2 * 64)] // Simplest case (1 sector => 2) [DataRow(Version.V3, 65)] [DataRow(Version.V3, 511)] [DataRow(Version.V3, 512)] // Multiple stream sectors diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index abda95af..fac360f1 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -61,11 +61,11 @@ public Header ReadHeader() throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})."); header.SectorShift = ReadUInt16(); header.MiniSectorShift = ReadUInt16(); - this.FillBuffer(6); + FillBuffer(6); header.DirectorySectorCount = ReadUInt32(); header.FatSectorCount = ReadUInt32(); header.FirstDirectorySectorId = ReadUInt32(); - this.FillBuffer(4); + FillBuffer(4); uint miniStreamCutoffSize = ReadUInt32(); if (miniStreamCutoffSize != Header.MiniStreamCutoffSize) throw new FormatException($"Mini stream cutoff size must be {Header.MiniStreamCutoffSize} bytes."); diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 9e78f0de..2319af4c 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -16,7 +16,7 @@ internal sealed class DirectoryEntryEnumerator : IEnumerator public DirectoryEntryEnumerator(IOContext ioContext) { this.ioContext = ioContext; - this.fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); } /// @@ -58,7 +58,7 @@ public bool MoveNext() return false; } - ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, index); index++; return true; @@ -113,7 +113,7 @@ public DirectoryEntry GetDictionaryEntry(uint streamId) if (!fatChainEnumerator.MoveTo(chainIndex)) throw new KeyNotFoundException($"Directory entry {streamId} was not found."); - ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, streamId); return current; } @@ -125,7 +125,7 @@ public void Write(DirectoryEntry entry) throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatChainEnumerator.CurrentSector.Position + entryIndex * DirectoryEntry.Length; + writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); writer.Write(entry); } diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index f5a7490f..c58262ec 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -8,7 +8,6 @@ namespace OpenMcdf3; /// internal sealed class DirectoryTreeEnumerator : IEnumerator { - private readonly IOContext ioContext; private readonly DirectoryEntry root; private DirectoryEntry? child; private readonly Stack stack = new(); @@ -18,7 +17,6 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) { directoryEntryEnumerator = new(ioContext); - this.ioContext = ioContext; this.root = root; if (root.ChildId != StreamId.NoStream) child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 5e2ec4c8..38223b7a 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -13,7 +13,7 @@ internal sealed class Fat : IEnumerable, IDisposable internal int FatElementsPerSector => ioContext.SectorSize / sizeof(uint); - internal int DifatElementsPerSector => ioContext.SectorSize / sizeof(uint) - 1; + internal int DifatElementsPerSector => (ioContext.SectorSize / sizeof(uint)) - 1; public Fat(IOContext ioContext) { @@ -64,7 +64,7 @@ public bool TryGetValue(uint key, out uint value) } CfbBinaryReader reader = ioContext.Reader; - reader.Position = fatSectorEnumerator.Current.Position + elementIndex * sizeof(uint); + reader.Position = fatSectorEnumerator.Current.Position + (elementIndex * sizeof(uint)); value = reader.ReadUInt32(); return true; } @@ -78,7 +78,7 @@ public bool TrySetValue(uint key, uint value) return false; CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatSectorEnumerator.Current.Position + elementIndex * sizeof(uint); + writer.Position = fatSectorEnumerator.Current.Position + (elementIndex * sizeof(uint)); writer.Write(value); return true; } diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index 1ccae887..2d538656 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -19,7 +19,7 @@ internal sealed class FatChainEnumerator : IEnumerator public FatChainEnumerator(IOContext ioContext, uint startSectorId) { this.ioContext = ioContext; - this.startId = startSectorId; + startId = startSectorId; fatEnumerator = new(ioContext); } @@ -179,7 +179,7 @@ public void Extend(uint requiredChainLength) chainLength++; } - this.length = requiredChainLength; + length = requiredChainLength; } public void Shrink(uint requiredChainLength) @@ -217,7 +217,7 @@ public void Shrink(uint requiredChainLength) Debug.Assert(length == requiredChainLength); #endif - this.length = requiredChainLength; + length = requiredChainLength; } /// diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 9ddf0cb4..ce78326f 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -16,7 +16,7 @@ internal sealed class FatSectorEnumerator : IEnumerator public FatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - this.difatSectorId = ioContext.Header.FirstDifatSectorId; + difatSectorId = ioContext.Header.FirstDifatSectorId; } /// @@ -42,7 +42,11 @@ public Sector Current /// public bool MoveNext() { - start = false; + if (start) + { + start = false; + index = uint.MaxValue; + } uint nextIndex = index + 1; if (nextIndex < ioContext.Header.FatSectorCount && nextIndex < Header.DifatArrayLength) // Include the free entries diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index bab51843..8675ba92 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -42,7 +42,7 @@ public uint this[uint key] throw new KeyNotFoundException($"Mini FAT index not found: {fatSectorIndex}."); CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatChainEnumerator.CurrentSector.Position + elementIndex * sizeof(uint); + writer.Position = fatChainEnumerator.CurrentSector.Position + (elementIndex * sizeof(uint)); writer.Write(value); } } @@ -60,7 +60,7 @@ public bool TryGetValue(uint key, out uint value) } CfbBinaryReader reader = ioContext.Reader; - reader.Position = fatChainEnumerator.CurrentSector.Position + elementIndex * sizeof(uint); + reader.Position = fatChainEnumerator.CurrentSector.Position + (elementIndex * sizeof(uint)); value = reader.ReadUInt32(); return true; } diff --git a/OpenMcdf3/MiniFatChainEnumerator.cs b/OpenMcdf3/MiniFatChainEnumerator.cs index a78be00f..e261243f 100644 --- a/OpenMcdf3/MiniFatChainEnumerator.cs +++ b/OpenMcdf3/MiniFatChainEnumerator.cs @@ -19,7 +19,7 @@ internal sealed class MiniFatChainEnumerator : IEnumerator public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) { this.ioContext = ioContext; - this.startId = startSectorId; + startId = startSectorId; miniFatEnumerator = new(ioContext); } @@ -156,7 +156,7 @@ public void Extend(uint requiredChainLength) Debug.Assert(length == requiredChainLength); #endif - this.length = requiredChainLength; + length = requiredChainLength; } public void Shrink(uint requiredChainLength) @@ -195,7 +195,7 @@ public void Shrink(uint requiredChainLength) Debug.Assert(length == requiredChainLength); #endif - this.length = requiredChainLength; + length = requiredChainLength; } /// diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index bd274f32..032e8370 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -1,7 +1,7 @@ namespace OpenMcdf3; /// -/// Provides a for reading a from the mini FAT stream. +/// Provides a for reading a from the mini FAT stream. /// internal sealed class MiniFatStream : Stream { diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf3/MiniSector.cs index a8d0e105..daf5b16e 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf3/MiniSector.cs @@ -4,6 +4,7 @@ /// Encapsulates information about a mini sector in a compound file. /// /// The ID of the mini sector +/// The sector length internal record struct MiniSector(uint Id, int Length) { public static readonly MiniSector EndOfChain = new(SectorType.EndOfChain, int.MaxValue); diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 6ce63152..f94318f3 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -7,7 +7,7 @@ public enum Version : ushort } /// -/// Encapsulates the root of a compound file. +/// Encapsulates the root of a compound file. /// public sealed class RootStorage : Storage, IDisposable { From 8d411d17c52dc085f80d1c258f40757734b0e6c9 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 4 Nov 2024 15:05:30 +1300 Subject: [PATCH 039/114] Improve EntryInfo --- OpenMcdf3/CompilerServices.cs | 9 +++++++++ OpenMcdf3/DirectoryEntry.cs | 2 +- OpenMcdf3/EntryInfo.cs | 7 +------ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 OpenMcdf3/CompilerServices.cs diff --git a/OpenMcdf3/CompilerServices.cs b/OpenMcdf3/CompilerServices.cs new file mode 100644 index 00000000..81332ada --- /dev/null +++ b/OpenMcdf3/CompilerServices.cs @@ -0,0 +1,9 @@ +#if NETSTANDARD2_0 || NETSTANDARD2_1 + +namespace System.Runtime.CompilerServices; + +internal class IsExternalInit +{ +} + +#endif \ No newline at end of file diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 6bdc87be..4a423d25 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -180,7 +180,7 @@ public void Recycle(StorageType storageType, string name) } } - public EntryInfo ToEntryInfo() => new() { Name = Name }; + public EntryInfo ToEntryInfo() => new(Name, StreamLength); public override string ToString() => $"{Id}: \"{Name}\""; diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs index b457ad3b..87e8f607 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf3/EntryInfo.cs @@ -3,9 +3,4 @@ /// /// Encapsulates information about an entry in a . /// -public class EntryInfo -{ - public string Name { get; internal set; } = string.Empty; - - public override string ToString() => Name; -} +public readonly record struct EntryInfo(string Name, long Length); From 68f339231cb9038ccf458f9971f5a5af5dae29f8 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 4 Nov 2024 16:44:39 +1300 Subject: [PATCH 040/114] Add modify stream test --- OpenMcdf3.Tests/StreamTests.cs | 75 +++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index d0249f72..3c83311a 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -183,7 +183,6 @@ public void WriteThenRead(Version version, int length) stream.Write(expectedBuffer, 0, expectedBuffer.Length); } - memoryStream.Position = 0; using (var rootStorage = RootStorage.Open(memoryStream)) { using CfbStream stream = rootStorage.OpenStream("TestStream"); @@ -248,6 +247,80 @@ public void WriteMultiple(Version version, int length) } } + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void Modify(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream1"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream2"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) + { + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + using (CfbStream stream = rootStorage.OpenStream("TestStream2")) + { + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + } + [TestMethod] [DataRow(Version.V3, 0)] [DataRow(Version.V3, 63)] From e84e8b86e87a44b4dcf5c041e14c04163d2d4f8b Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 4 Nov 2024 20:29:42 +1300 Subject: [PATCH 041/114] Check stream is seekable --- OpenMcdf3/RootStorage.cs | 5 ++++- OpenMcdf3/ThrowHelper.cs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index f94318f3..3a87930c 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -19,8 +19,10 @@ public static RootStorage Create(string fileName, Version version = Version.V3) public static RootStorage Create(Stream stream, Version version = Version.V3) { - Header header = new(version); + stream.ThrowIfNotSeekable(); stream.SetLength(0); + + Header header = new(version); CfbBinaryReader reader = new(stream); CfbBinaryWriter writer = new(stream); IOContext ioContext = new(header, reader, writer, IOContextFlags.Create); @@ -41,6 +43,7 @@ public static RootStorage OpenRead(string fileName) public static RootStorage Open(Stream stream, bool leaveOpen = false) { + stream.ThrowIfNotSeekable(); stream.Position = 0; Header header; diff --git a/OpenMcdf3/ThrowHelper.cs b/OpenMcdf3/ThrowHelper.cs index b7c61608..83ada20e 100644 --- a/OpenMcdf3/ThrowHelper.cs +++ b/OpenMcdf3/ThrowHelper.cs @@ -25,6 +25,12 @@ public static void ThrowIfStreamArgumentsAreInvalid(byte[] buffer, int offset, i 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."); } + public static void ThrowIfNotSeekable(this Stream stream) + { + if (!stream.CanSeek) + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } + public static void ThrowIfNotWritable(this Stream stream) { if (!stream.CanWrite) From 8aa1f26d7ba24fde5959951d4a34f695f2e9675c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 4 Nov 2024 16:44:53 +1300 Subject: [PATCH 042/114] Add transaction stream --- OpenMcdf3.Tests/StreamTests.cs | 144 ++++++++++++++++++++++++++ OpenMcdf3/DirectoryEntryEnumerator.cs | 1 + OpenMcdf3/FatStream.cs | 2 +- OpenMcdf3/IOContext.cs | 46 ++++++-- OpenMcdf3/RootStorage.cs | 54 +++++++--- OpenMcdf3/TransactedStream.cs | 128 +++++++++++++++++++++++ 6 files changed, 347 insertions(+), 28 deletions(-) create mode 100644 OpenMcdf3/TransactedStream.cs diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 3c83311a..058182ab 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -321,6 +321,150 @@ public void Modify(Version version, int length) } } + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void ModifyCommit(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream1"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.Transacted)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream2"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + stream.Flush(); + rootStorage.Commit(); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) + { + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + using (CfbStream stream = rootStorage.OpenStream("TestStream2")) + { + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + } + + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void ModifyRevert(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream1"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.Transacted)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream2"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + rootStorage.Revert(); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) + { + Assert.AreEqual(length, stream.Length); + + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + + Assert.ThrowsException(() => rootStorage.OpenStream("TestStream2")); + } + } + [TestMethod] [DataRow(Version.V3, 0)] [DataRow(Version.V3, 63)] diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 2319af4c..39f858a4 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; namespace OpenMcdf3; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index cf8f506e..afefd4f1 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -168,7 +168,7 @@ public override void Write(byte[] buffer, int offset, int count) if (!chain.MoveTo(chainIndex)) throw new InvalidOperationException($"Failed to move to FAT chain index: {chainIndex}"); - CfbBinaryWriter writer = ioContext.Writer!; + CfbBinaryWriter writer = ioContext.Writer; int writeCount = 0; do { diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 817dc110..b42eb961 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -4,7 +4,8 @@ enum IOContextFlags { None = 0, Create = 1, - LeaveOpen = 2 + LeaveOpen = 2, + Transacted = 4, } /// @@ -12,6 +13,7 @@ enum IOContextFlags /// internal sealed class IOContext : IDisposable { + readonly Stream stream; readonly DirectoryEntryEnumerator directoryEnumerator; readonly CfbBinaryWriter? writer; MiniFat? miniFat; @@ -66,17 +68,25 @@ public FatStream MiniStream public Version Version => (Version)Header.MajorVersion; - public IOContext(Header header, CfbBinaryReader reader, CfbBinaryWriter? writer, IOContextFlags contextFlags = IOContextFlags.None) + public IOContext(Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) { - if (contextFlags.HasFlag(IOContextFlags.Create) && writer is null) - throw new ArgumentNullException(nameof(writer), "A writer is required to create a new compound file."); + this.stream = stream; - Header = header; - Reader = reader; - this.writer = writer; + using CfbBinaryReader reader = new(stream); + Header = contextFlags.HasFlag(IOContextFlags.Create) ? new(version) : reader.ReadHeader(); + SectorSize = 1 << Header.SectorShift; + MiniSectorSize = 1 << Header.MiniSectorShift; - SectorSize = 1 << header.SectorShift; - MiniSectorSize = 1 << header.MiniSectorShift; + Stream transactedStream = stream; + if (contextFlags.HasFlag(IOContextFlags.Transacted)) + { + Stream overlayStream = stream is MemoryStream ? new MemoryStream() : File.Create(Path.GetTempFileName()); + transactedStream = new TransactedStream(this, stream, overlayStream); + } + + Reader = new(transactedStream); + if (stream.CanWrite) + writer = new(transactedStream); Fat = new(this); directoryEnumerator = new(this); @@ -101,7 +111,7 @@ public void Dispose() { if (!IsDisposed) { - if (CanWrite) + if (writer is not null && writer.BaseStream is not TransactedStream) WriteHeader(); miniStream?.Dispose(); miniFat?.Dispose(); @@ -131,4 +141,20 @@ public void Write(DirectoryEntry entry) { directoryEnumerator.Write(entry); } + + public void Commit() + { + if (writer is null || writer.BaseStream is not TransactedStream transactedStream) + throw new InvalidOperationException("Cannot commit non-transacted storage."); + + WriteHeader(); + transactedStream.Commit(); + } + public void Revert() + { + if (writer is null || writer.BaseStream is not TransactedStream transactedStream) + throw new InvalidOperationException("Cannot commit non-transacted storage."); + + transactedStream.Revert(); + } } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 3a87930c..0201bcef 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -2,34 +2,47 @@ public enum Version : ushort { + Unknown = 0, V3 = 3, V4 = 4 } +[Flags] +public enum StorageModeFlags +{ + None = 0, + LeaveOpen = 0x01, + Transacted = 0x02, +} + /// /// Encapsulates the root of a compound file. /// public sealed class RootStorage : Storage, IDisposable { - public static RootStorage Create(string fileName, Version version = Version.V3) + public static RootStorage Create(string fileName, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) { FileStream stream = File.Create(fileName); return Create(stream, version); } - public static RootStorage Create(Stream stream, Version version = Version.V3) + public static RootStorage Create(Stream stream, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) { stream.ThrowIfNotSeekable(); stream.SetLength(0); + stream.Position = 0; + + IOContextFlags contextFlags = IOContextFlags.Create; + if (flags.HasFlag(StorageModeFlags.LeaveOpen)) + contextFlags |= IOContextFlags.LeaveOpen; + if (flags.HasFlag(StorageModeFlags.Transacted)) + contextFlags |= IOContextFlags.Transacted; - Header header = new(version); - CfbBinaryReader reader = new(stream); - CfbBinaryWriter writer = new(stream); - IOContext ioContext = new(header, reader, writer, IOContextFlags.Create); + IOContext ioContext = new(stream, version, contextFlags); return new RootStorage(ioContext); } - public static RootStorage Open(string fileName, FileMode mode) + public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags flags = StorageModeFlags.None) { FileStream stream = File.Open(fileName, mode); return Open(stream); @@ -41,21 +54,18 @@ public static RootStorage OpenRead(string fileName) return Open(stream); } - public static RootStorage Open(Stream stream, bool leaveOpen = false) + public static RootStorage Open(Stream stream, StorageModeFlags flags = StorageModeFlags.None) { stream.ThrowIfNotSeekable(); stream.Position = 0; - Header header; - using (CfbBinaryReader headerReader = new(stream)) - { - header = headerReader.ReadHeader(); - } + IOContextFlags contextFlags = IOContextFlags.None; + if (flags.HasFlag(StorageModeFlags.LeaveOpen)) + contextFlags |= IOContextFlags.LeaveOpen; + if (flags.HasFlag(StorageModeFlags.Transacted)) + contextFlags |= IOContextFlags.Transacted; - CfbBinaryReader reader = new(stream); - CfbBinaryWriter? writer = stream.CanWrite ? new(stream) : null; - IOContextFlags contextFlags = leaveOpen ? IOContextFlags.LeaveOpen : IOContextFlags.None; - IOContext ioContext = new(header, reader, writer, contextFlags); + IOContext ioContext = new(stream, Version.Unknown, contextFlags); return new RootStorage(ioContext); } @@ -66,6 +76,16 @@ public static RootStorage Open(Stream stream, bool leaveOpen = false) public void Dispose() => ioContext?.Dispose(); + public void Commit() + { + ioContext.Commit(); + } + + public void Revert() + { + ioContext.Revert(); + } + internal void Trace(TextWriter writer) { writer.WriteLine(ioContext.Header); diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs new file mode 100644 index 00000000..15c35227 --- /dev/null +++ b/OpenMcdf3/TransactedStream.cs @@ -0,0 +1,128 @@ +using System.Diagnostics; + +namespace OpenMcdf3; + +internal class TransactedStream : Stream +{ + readonly IOContext ioContext; + readonly Stream originalStream; + readonly Stream overlayStream; + readonly Dictionary dirtySectorPositions = new(); + readonly byte[] buffer; + + public TransactedStream(IOContext ioContext, Stream originalStream, Stream overlayStream) + { + this.ioContext = ioContext; + this.originalStream = originalStream; + this.overlayStream = overlayStream; + buffer = new byte[ioContext.SectorSize]; + } + + protected override void Dispose(bool disposing) + { + // Original stream might be owned by the caller + overlayStream.Dispose(); + + base.Dispose(disposing); + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => true; + + public override long Length => overlayStream.Length; + + public override long Position { get => originalStream.Position; set => originalStream.Position = value; } + + public override void Flush() => overlayStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + + uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); + int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + int localCount = Math.Min(count, remainingFromSector); + Debug.Assert(localCount == count); + + int read; + if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) + { + overlayStream.Position = overlayPosition + sectorOffset; + read = overlayStream.Read(buffer, offset, localCount); + originalStream.Seek(read, SeekOrigin.Current); + } + else + { + read = originalStream.Read(buffer, offset, localCount); + } + + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return originalStream.Seek(offset, origin); + } + + public override void SetLength(long value) => overlayStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + + uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); + int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + int localCount = Math.Min(count, remainingFromSector); + Debug.Assert(localCount == count); + // TODO: Loop through the buffer and write to the overlay stream + + bool added = false; + if (!dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) + { + overlayPosition = overlayStream.Length; + dirtySectorPositions.Add(sectorId, overlayPosition); + added = true; + } + + if (added && originalStream.Position < originalStream.Length && localCount != ioContext.SectorSize) + { + // Copy the existing sector data + long originalPosition = originalStream.Position; + originalStream.Position = originalPosition - sectorOffset; + originalStream.ReadExactly(this.buffer); + originalStream.Position = originalPosition; + + overlayStream.Position = overlayPosition; + overlayStream.Write(this.buffer, 0, this.buffer.Length); + } + + if (overlayStream.Length < overlayPosition + ioContext.SectorSize) + overlayStream.SetLength(overlayPosition + ioContext.SectorSize); + overlayStream.Position = overlayPosition + sectorOffset; + overlayStream.Write(buffer, offset, localCount); + originalStream.Seek(localCount, SeekOrigin.Current); + } + + public void Commit() + { + foreach (KeyValuePair entry in dirtySectorPositions) + { + overlayStream.Position = entry.Value; + overlayStream.ReadExactly(buffer); + + originalStream.Position = entry.Key * ioContext.SectorSize; + originalStream.Write(buffer, 0, buffer.Length); + } + + originalStream.Flush(); + dirtySectorPositions.Clear(); + } + + public void Revert() + { + dirtySectorPositions.Clear(); + } +} \ No newline at end of file From f71220091fb3e35d86a84fee643c2e138dbac189 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 11:48:17 +1300 Subject: [PATCH 043/114] Add transaction benchmark --- OpenMcdf3.Benchmarks/InMemory.cs | 62 ++++++++----------- .../OpenMcdf3.Benchmarks.csproj | 1 - 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index 4d3297e5..be786753 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -using OpenMcdf; namespace OpenMcdf3.Benchmark; @@ -18,10 +17,10 @@ public class InMemory : IDisposable private const string storageName = "MyStorage"; private const string streamName = "MyStream"; - private byte[] readBuffer; + private MemoryStream readStream = new(); + private MemoryStream writeStream = new(); - private readonly MemoryStream readStream = new(); - private readonly MemoryStream writeStream = new(); + private byte[] buffer = Array.Empty(); [Params(512, Mb /*Kb, 4 * Kb, 128 * Kb, 256 * Kb, 512 * Kb,*/)] public int BufferSize { get; set; } @@ -38,58 +37,49 @@ public void Dispose() [GlobalSetup] public void GlobalSetup() { - readBuffer = new byte[BufferSize]; - CreateFile(1); + buffer = new byte[BufferSize]; + readStream = new MemoryStream(2 * TotalStreamSize); + writeStream = new MemoryStream(2 * TotalStreamSize); + + using var storage = RootStorage.Create(readStream, Version.V3, StorageModeFlags.LeaveOpen); + using CfbStream stream = storage.CreateStream(streamName); + + int iterationCount = TotalStreamSize / BufferSize; + for (int iteration = 0; iteration < iterationCount; ++iteration) + stream.Write(buffer); } [Benchmark] public void Read() { using var compoundFile = RootStorage.Open(readStream); - using CfbStream cfStream = compoundFile.OpenStream(streamName + 0); + using CfbStream cfStream = compoundFile.OpenStream(streamName); long streamSize = cfStream.Length; long position = 0L; - while (true) + while (position < streamSize) { - if (position >= streamSize) - break; - int read = cfStream.Read(readBuffer, 0, readBuffer.Length); + int read = cfStream.Read(buffer, 0, buffer.Length); + if (read <= 0) + throw new EndOfStreamException(); position += read; - if (read <= 0) break; } } [Benchmark] - public void Write() + public void Write() => WriteCore(StorageModeFlags.None); + + [Benchmark] + public void WriteTransacted() => WriteCore(StorageModeFlags.Transacted); + + void WriteCore(StorageModeFlags flags) { - MemoryStream memoryStream = writeStream; - using var storage = RootStorage.Create(memoryStream); + using var storage = RootStorage.Create(writeStream, Version.V3, flags); Storage subStorage = storage.CreateStorage(storageName); CfbStream stream = subStorage.CreateStream(streamName + 0); while (stream.Length < TotalStreamSize) { - stream.Write(readBuffer, 0, readBuffer.Length); - } - } - - private void CreateFile(int streamCount) - { - int iterationCount = TotalStreamSize / BufferSize; - - byte[] buffer = new byte[BufferSize]; - buffer.AsSpan().Fill(byte.MaxValue); - const CFSConfiguration flags = CFSConfiguration.Default | CFSConfiguration.LeaveOpen; - using var compoundFile = new CompoundFile(CFSVersion.Ver_3, flags); - CFStorage st = compoundFile.RootStorage; - for (int streamId = 0; streamId < streamCount; ++streamId) - { - CFStream sm = st.AddStream(streamName + streamId); - for (int iteration = 0; iteration < iterationCount; ++iteration) - sm.Append(buffer); + stream.Write(buffer, 0, buffer.Length); } - - compoundFile.Save(readStream); - compoundFile.Close(); } } diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj index 8d732776..209959c3 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj +++ b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj @@ -10,7 +10,6 @@ - From e489569a05d022ae591b833e2a319f21c49241da Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 14:28:12 +1300 Subject: [PATCH 044/114] Fix write overloads --- OpenMcdf3/CfbBinaryWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index b041469e..bb079926 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -20,13 +20,13 @@ public long Position set => BaseStream.Position = value; } -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); #endif public void Write(Guid value) { -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER Span localBuffer = stackalloc byte[16]; value.TryWriteBytes(localBuffer); Write(localBuffer); From 982b4ec5c3abf1ee739653be803076a13ab90f5f Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 14:43:17 +1300 Subject: [PATCH 045/114] Use stackalloc for writing bytes --- OpenMcdf3/CfbBinaryWriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index bb079926..a8bd2327 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -22,6 +22,12 @@ public long Position #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); + + public override void Write(byte value) + { + Span localBuffer = stackalloc byte[1] { value }; + Write(localBuffer); + } #endif public void Write(Guid value) From d46ecc6f82a02d571277524da735677dc27773fc Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 15:12:00 +1300 Subject: [PATCH 046/114] Add netstandard2.1 stream overrides --- OpenMcdf3/CfbBinaryWriter.cs | 4 +- OpenMcdf3/CfbStream.cs | 21 +++++++++ OpenMcdf3/FatStream.cs | 87 ++++++++++++++++++++++++++++++++++- OpenMcdf3/MiniFatStream.cs | 86 ++++++++++++++++++++++++++++++++++ OpenMcdf3/StreamExtensions.cs | 24 ++++++++-- OpenMcdf3/TransactedStream.cs | 66 ++++++++++++++++++++++++++ 6 files changed, 283 insertions(+), 5 deletions(-) diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index a8bd2327..f17a0339 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -21,6 +21,7 @@ public long Position } #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); public override void Write(byte value) @@ -28,9 +29,10 @@ public override void Write(byte value) Span localBuffer = stackalloc byte[1] { value }; Write(localBuffer); } + #endif - public void Write(Guid value) + public void Write(in Guid value) { #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER Span localBuffer = stackalloc byte[16]; diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index 473eae7f..933ef167 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -93,4 +93,25 @@ public override void Write(byte[] buffer, int offset, int count) stream.Write(buffer, offset, count); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + + public override int Read(Span buffer) => stream.Read(buffer); + + public override int ReadByte() => this.ReadByteCore(); + + public override void WriteByte(byte value) => this.WriteByteCore(value); + + public override void Write(ReadOnlySpan buffer) + { + this.ThrowIfNotWritable(); + + long newPosition = Position + buffer.Length; + if (newPosition > stream.Length) + SetLength(newPosition); + + stream.Write(buffer); + } + +#endif } diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index afefd4f1..c73ba459 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf3; +using System.IO; + +namespace OpenMcdf3; /// /// Provides a for a stream object in a compound file./> @@ -189,4 +191,87 @@ public override void Write(byte[] buffer, int offset, int count) throw new InvalidOperationException($"End of FAT chain was reached"); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + + public override int ReadByte() => this.ReadByteCore(); + + public override int Read(Span buffer) + { + this.ThrowIfDisposed(disposed); + + if (buffer.Length == 0) + return 0; + + int maxCount = (int)Math.Min(Math.Max(Length - position, 0), int.MaxValue); + if (maxCount == 0) + return 0; + + uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + return 0; + + int realCount = Math.Min(buffer.Length, maxCount); + int readCount = 0; + do + { + Sector sector = chain.CurrentSector; + int remaining = realCount - readCount; + long readLength = Math.Min(remaining, sector.Length - sectorOffset); + ioContext.Reader.Position = sector.Position + sectorOffset; + int localOffset = readCount; + Span slice = buffer.Slice(localOffset, (int)readLength); + int read = ioContext.Reader.Read(slice); + if (read == 0) + return readCount; + position += read; + readCount += read; + sectorOffset = 0; + if (readCount >= realCount) + return readCount; + } while (chain.MoveNext()); + + return readCount; + } + + public override void WriteByte(byte value) => this.WriteByteCore(value); + + public override void Write(ReadOnlySpan buffer) + { + this.ThrowIfNotWritable(); + + if (buffer.Length == 0) + return; + + if (position + buffer.Length > ChainCapacity) + SetLength(position + buffer.Length); + + uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + if (!chain.MoveTo(chainIndex)) + throw new InvalidOperationException($"Failed to move to FAT chain index: {chainIndex}"); + + CfbBinaryWriter writer = ioContext.Writer; + int writeCount = 0; + do + { + Sector sector = chain.CurrentSector; + writer.Position = sector.Position + sectorOffset; + int remaining = buffer.Length - writeCount; + int localOffset = writeCount; + long writeLength = Math.Min(remaining, sector.Length - sectorOffset); + ReadOnlySpan slice = buffer.Slice(localOffset, (int)writeLength); + writer.Write(slice); + position += writeLength; + writeCount += (int)writeLength; + if (position > Length) + DirectoryEntry.StreamLength = position; + sectorOffset = 0; + if (writeCount >= buffer.Length) + return; + } while (chain.MoveNext()); + + throw new InvalidOperationException($"End of FAT chain was reached"); + } + +#endif } diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index 032e8370..e4357f62 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -185,4 +185,90 @@ public override void Write(byte[] buffer, int offset, int count) throw new InvalidOperationException($"End of mini FAT chain was reached."); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + + public override int ReadByte() => this.ReadByteCore(); + + public override int Read(Span buffer) + { + this.ThrowIfDisposed(disposed); + + if (buffer.Length == 0) + return 0; + + int maxCount = (int)Math.Min(Math.Max(Length - position, 0), int.MaxValue); + if (maxCount == 0) + return 0; + + uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + if (!miniChain.MoveTo(chainIndex)) + return 0; + + FatStream miniStream = ioContext.MiniStream; + int realCount = Math.Min(buffer.Length, maxCount); + int readCount = 0; + do + { + MiniSector miniSector = miniChain.CurrentSector; + int remaining = realCount - readCount; + long readLength = Math.Min(remaining, miniSector.Length - sectorOffset); + miniStream.Position = miniSector.Position + sectorOffset; + int localOffset = readCount; + Span slice = buffer.Slice(localOffset, (int)readLength); + int read = miniStream.Read(slice); + if (read == 0) + return readCount; + position += read; + readCount += read; + sectorOffset = 0; + if (readCount >= realCount) + return readCount; + } while (miniChain.MoveNext()); + + return readCount; + } + + public override void WriteByte(byte value) => this.WriteByteCore(value); + + public override void Write(ReadOnlySpan buffer) + { + this.ThrowIfDisposed(disposed); + this.ThrowIfNotWritable(); + + if (buffer.Length == 0) + return; + + if (position + buffer.Length > ChainCapacity) + SetLength(position + buffer.Length); + + uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + if (!miniChain.MoveTo(chainIndex)) + throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); + + FatStream miniStream = ioContext.MiniStream; + int writeCount = 0; + do + { + MiniSector miniSector = miniChain.CurrentSector; + long basePosition = miniSector.Position + sectorOffset; + miniStream.Seek(basePosition, SeekOrigin.Begin); + int remaining = buffer.Length - writeCount; + int localOffset = writeCount; + long writeLength = Math.Min(remaining, ioContext.MiniSectorSize - sectorOffset); + ReadOnlySpan slice = buffer.Slice(localOffset, (int)writeLength); + miniStream.Write(slice); + position += writeLength; + writeCount += (int)writeLength; + if (position > Length) + DirectoryEntry.StreamLength = position; + sectorOffset = 0; + if (writeCount >= buffer.Length) + return; + } while (miniChain.MoveNext()); + + throw new InvalidOperationException($"End of mini FAT chain was reached."); + } + +#endif } diff --git a/OpenMcdf3/StreamExtensions.cs b/OpenMcdf3/StreamExtensions.cs index b2fd63c4..ec600f63 100644 --- a/OpenMcdf3/StreamExtensions.cs +++ b/OpenMcdf3/StreamExtensions.cs @@ -2,10 +2,10 @@ namespace OpenMcdf3; -#if !NET7_0_OR_GREATER - internal static class StreamExtensions { +#if !NET7_0_OR_GREATER + public static void ReadExactly(this Stream stream, Span buffer) { byte[] array = ArrayPool.Shared.Rent(buffer.Length); @@ -35,6 +35,24 @@ public static void ReadExactly(this Stream stream, byte[] buffer, int offset, in totalRead += read; } while (totalRead < count); } -} #endif + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + + public static int ReadByteCore(this Stream stream) + { + Span bytes = stackalloc byte[1]; + int read = stream.Read(bytes); + return read == 0 ? -1 : bytes[0]; + } + + public static void WriteByteCore(this Stream stream, byte value) + { + ReadOnlySpan bytes = stackalloc byte[] { value }; + stream.Write(bytes); + } + +#endif +} + diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 15c35227..6af5179c 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -125,4 +125,70 @@ public void Revert() { dirtySectorPositions.Clear(); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + + public override int ReadByte() => this.ReadByteCore(); + + public override int Read(Span buffer) + { + uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); + int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + int localCount = Math.Min(buffer.Length, remainingFromSector); + Debug.Assert(localCount == buffer.Length); + + Span slice = buffer.Slice(0, localCount); + int read; + if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) + { + overlayStream.Position = overlayPosition + sectorOffset; + read = overlayStream.Read(slice); + originalStream.Seek(read, SeekOrigin.Current); + } + else + { + read = originalStream.Read(slice); + } + + return read; + } + + public override void WriteByte(byte value) => this.WriteByteCore(value); + + public override void Write(ReadOnlySpan buffer) + { + uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); + int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + int localCount = Math.Min(buffer.Length, remainingFromSector); + Debug.Assert(localCount == buffer.Length); + // TODO: Loop through the buffer and write to the overlay stream + + bool added = false; + if (!dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) + { + overlayPosition = overlayStream.Length; + dirtySectorPositions.Add(sectorId, overlayPosition); + added = true; + } + + if (added && originalStream.Position < originalStream.Length && localCount != ioContext.SectorSize) + { + // Copy the existing sector data + long originalPosition = originalStream.Position; + originalStream.Position = originalPosition - sectorOffset; + originalStream.ReadExactly(this.buffer); + originalStream.Position = originalPosition; + + overlayStream.Position = overlayPosition; + overlayStream.Write(this.buffer, 0, this.buffer.Length); + } + + if (overlayStream.Length < overlayPosition + ioContext.SectorSize) + overlayStream.SetLength(overlayPosition + ioContext.SectorSize); + overlayStream.Position = overlayPosition + sectorOffset; + overlayStream.Write(buffer); + originalStream.Seek(localCount, SeekOrigin.Current); + } + +#endif } \ No newline at end of file From 97794f70d5c2a92f4d054df45f4aa0c919462d99 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 16:25:53 +1300 Subject: [PATCH 047/114] Benchmark stuff --- OpenMcdf3.Benchmarks/InMemory.cs | 10 +++++----- OpenMcdf3.Perf/Program.cs | 6 +++--- OpenMcdf3/IOContext.cs | 2 +- OpenMcdf3/OpenMcdf3.csproj | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index be786753..3dbe73d6 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -3,7 +3,7 @@ namespace OpenMcdf3.Benchmark; -[SimpleJob] +[ShortRunJob] [CsvExporter] [HtmlExporter] [MarkdownExporter] @@ -17,8 +17,8 @@ public class InMemory : IDisposable private const string storageName = "MyStorage"; private const string streamName = "MyStream"; - private MemoryStream readStream = new(); - private MemoryStream writeStream = new(); + private FileStream readStream = File.Create(Path.GetTempFileName()); + private FileStream writeStream = File.Create(Path.GetTempFileName()); private byte[] buffer = Array.Empty(); @@ -38,8 +38,8 @@ public void Dispose() public void GlobalSetup() { buffer = new byte[BufferSize]; - readStream = new MemoryStream(2 * TotalStreamSize); - writeStream = new MemoryStream(2 * TotalStreamSize); + //readStream = new MemoryStream(2 * TotalStreamSize); + //writeStream = new MemoryStream(2 * TotalStreamSize); using var storage = RootStorage.Create(readStream, Version.V3, StorageModeFlags.LeaveOpen); using CfbStream stream = storage.CreateStream(streamName); diff --git a/OpenMcdf3.Perf/Program.cs b/OpenMcdf3.Perf/Program.cs index 63bb11d7..d26fa186 100644 --- a/OpenMcdf3.Perf/Program.cs +++ b/OpenMcdf3.Perf/Program.cs @@ -7,7 +7,7 @@ internal sealed class Program static void Main(string[] args) { var stopwatch = Stopwatch.StartNew(); - Write(Version.V3, 512, 1024); + Write(Version.V3, 512, 2 * 1024); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); } @@ -20,8 +20,8 @@ static void Write(Version version, int length, int iterations) //byte[] actualBuffer = new byte[length]; - using MemoryStream memoryStream = new(2 * length); - using var rootStorage = RootStorage.Create(memoryStream, version); + using MemoryStream memoryStream = new(2 * length * iterations); + using var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.Transacted); using Stream stream = rootStorage.CreateStream("TestStream"); for (int i = 0; i < iterations; i++) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index b42eb961..72111bc6 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -80,7 +80,7 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I Stream transactedStream = stream; if (contextFlags.HasFlag(IOContextFlags.Transacted)) { - Stream overlayStream = stream is MemoryStream ? new MemoryStream() : File.Create(Path.GetTempFileName()); + Stream overlayStream = stream is MemoryStream ? new MemoryStream((int)stream.Length) : File.Create(Path.GetTempFileName()); transactedStream = new TransactedStream(this, stream, overlayStream); } diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj index 081a0aef..48b852fb 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1;net8.0 + netstandard2.0;net8.0 11.0 enable enable From 45ace4ffcdec0e94203020e8fab25e9b098f4e84 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 21:25:50 +1300 Subject: [PATCH 048/114] Minor refactor --- OpenMcdf3/FatEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMcdf3/FatEntry.cs b/OpenMcdf3/FatEntry.cs index ecef6605..db01412c 100644 --- a/OpenMcdf3/FatEntry.cs +++ b/OpenMcdf3/FatEntry.cs @@ -9,5 +9,5 @@ internal record struct FatEntry(uint Index, uint Value) public readonly bool IsFree => Value == SectorType.Free; - public readonly override string ToString() => $"#{Index}: {Value}"; + public override readonly string ToString() => $"#{Index}: {Value}"; } From 315fd663d0c54ac185996ea848b3da21713a4430 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 21:26:23 +1300 Subject: [PATCH 049/114] Optimize directory entry writes --- OpenMcdf3/FatStream.cs | 17 ++++++++++------- OpenMcdf3/MiniFatStream.cs | 12 ++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index c73ba459..ad98aed5 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -48,6 +48,8 @@ protected override void Dispose(bool disposing) { if (!disposed) { + Flush(); + chain.Dispose(); disposed = true; } @@ -59,7 +61,12 @@ protected override void Dispose(bool disposing) public override void Flush() { this.ThrowIfDisposed(disposed); - ioContext.Writer!.Flush(); // TODO: Check validity + + if (CanWrite) + { + ioContext.Write(DirectoryEntry); + ioContext.Writer!.Flush(); + } } /// @@ -145,12 +152,8 @@ public override void SetLength(long value) else if (value <= ChainCapacity - ioContext.SectorSize) chain.Shrink(requiredChainLength); - if (DirectoryEntry.StartSectorId != chain.StartId || DirectoryEntry.StreamLength != value) - { - DirectoryEntry.StartSectorId = chain.StartId; - DirectoryEntry.StreamLength = value; - ioContext.Write(DirectoryEntry); - } + DirectoryEntry.StartSectorId = chain.StartId; + DirectoryEntry.StreamLength = value; } /// diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index e4357f62..c7c00984 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -39,6 +39,8 @@ protected override void Dispose(bool disposing) { if (!disposed) { + Flush(); + miniChain.Dispose(); disposed = true; } @@ -50,6 +52,8 @@ public override void Flush() { this.ThrowIfDisposed(disposed); + if (CanWrite) + ioContext.Write(DirectoryEntry); ioContext.MiniStream.Flush(); } @@ -138,12 +142,8 @@ public override void SetLength(long value) else if (value <= ChainCapacity - ioContext.MiniSectorSize) miniChain.Shrink(requiredChainLength); - if (DirectoryEntry.StartSectorId != miniChain.StartId || DirectoryEntry.StreamLength != value) - { - DirectoryEntry.StartSectorId = miniChain.StartId; - DirectoryEntry.StreamLength = value; - ioContext.Write(DirectoryEntry); - } + DirectoryEntry.StartSectorId = miniChain.StartId; + DirectoryEntry.StreamLength = value; } public override void Write(byte[] buffer, int offset, int count) From d496dda45b4a8ce2f8efd9d088c128cf1ea7c2bd Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 21:58:31 +1300 Subject: [PATCH 050/114] Minor performance improvements --- OpenMcdf3/Fat.cs | 33 ++++++++++++++++++--------------- OpenMcdf3/MiniFat.cs | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 38223b7a..48993745 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -10,14 +10,16 @@ internal sealed class Fat : IEnumerable, IDisposable { private readonly IOContext ioContext; private readonly FatSectorEnumerator fatSectorEnumerator; - - internal int FatElementsPerSector => ioContext.SectorSize / sizeof(uint); - - internal int DifatElementsPerSector => (ioContext.SectorSize / sizeof(uint)) - 1; + private readonly int DifatArrayElementCount; + private readonly int FatElementsPerSector; + private readonly int DifatElementsPerSector; public Fat(IOContext ioContext) { this.ioContext = ioContext; + FatElementsPerSector = ioContext.SectorSize / sizeof(uint); + DifatElementsPerSector = FatElementsPerSector - 1; + DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; fatSectorEnumerator = new(ioContext); } @@ -44,19 +46,22 @@ public uint this[uint key] uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) { - int DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; if (key < DifatArrayElementCount) return (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); - return (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); } + bool TryMoveToSectorForKey(uint key, out long offset) + { + uint sectorId = GetSectorIndexAndElementOffset(key, out offset); + return fatSectorEnumerator.MoveTo(sectorId); + } + public bool TryGetValue(uint key, out uint value) { ThrowHelper.ThrowIfSectorIdIsInvalid(key); - uint sectorId = GetSectorIndexAndElementOffset(key, out long elementIndex); - bool ok = fatSectorEnumerator.MoveTo(sectorId); + bool ok = TryMoveToSectorForKey(key, out long elementIndex); if (!ok) { value = uint.MaxValue; @@ -73,8 +78,7 @@ public bool TrySetValue(uint key, uint value) { ThrowHelper.ThrowIfSectorIdIsInvalid(key); - uint fatSectorIndex = GetSectorIndexAndElementOffset(key, out long elementIndex); - if (!fatSectorEnumerator.MoveTo(fatSectorIndex)) + if (!TryMoveToSectorForKey(key, out long elementIndex)) return false; CfbBinaryWriter writer = ioContext.Writer; @@ -91,15 +95,14 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) { ThrowHelper.ThrowIfSectorIdIsInvalid(startIndex); - bool movedToFreeEntry = fatEnumerator.MoveTo(startIndex) && fatEnumerator.MoveNextFreeEntry(); + bool movedToFreeEntry = fatEnumerator.MoveTo(startIndex) + && fatEnumerator.MoveNextFreeEntry(); if (!movedToFreeEntry) { uint newSectorId = fatSectorEnumerator.Add(); - bool ok = fatEnumerator.MoveTo(newSectorId); - Debug.Assert(ok); - - ok = fatEnumerator.MoveNextFreeEntry(); + // Next id must be free + bool ok = fatEnumerator.MoveTo(newSectorId + 1); Debug.Assert(ok); } diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index 8675ba92..5fe425a0 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -10,12 +10,12 @@ internal sealed class MiniFat : IEnumerable, IDisposable { private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; - - internal int ElementsPerSector => ioContext.SectorSize / sizeof(uint); + private readonly int ElementsPerSector; public MiniFat(IOContext ioContext) { this.ioContext = ioContext; + ElementsPerSector = ioContext.SectorSize / sizeof(uint); fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); } From e40aae66600e9c433a2065a347a69063a592d7cd Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 22:10:21 +1300 Subject: [PATCH 051/114] Perf exe improvements --- OpenMcdf3.Perf/Program.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3.Perf/Program.cs b/OpenMcdf3.Perf/Program.cs index d26fa186..2e356c9a 100644 --- a/OpenMcdf3.Perf/Program.cs +++ b/OpenMcdf3.Perf/Program.cs @@ -7,11 +7,11 @@ internal sealed class Program static void Main(string[] args) { var stopwatch = Stopwatch.StartNew(); - Write(Version.V3, 512, 2 * 1024); + Write(Version.V3, 4096, 1024 / 4, StorageModeFlags.Transacted); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); } - static void Write(Version version, int length, int iterations) + static void Write(Version version, int length, int iterations, StorageModeFlags storageModeFlags) { // Fill with bytes equal to their position modulo 256 byte[] expectedBuffer = new byte[length]; @@ -21,12 +21,15 @@ static void Write(Version version, int length, int iterations) //byte[] actualBuffer = new byte[length]; using MemoryStream memoryStream = new(2 * length * iterations); - using var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.Transacted); + using var rootStorage = RootStorage.Create(memoryStream, version, storageModeFlags); using Stream stream = rootStorage.CreateStream("TestStream"); for (int i = 0; i < iterations; i++) { stream.Write(expectedBuffer, 0, expectedBuffer.Length); } + + if (storageModeFlags.HasFlag(StorageModeFlags.Transacted)) + rootStorage.Commit(); } } From f1f8a67e24f690ba9e6c895c7342c2d1eb14d7d8 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 22:46:16 +1300 Subject: [PATCH 052/114] Cache current FAT sector --- OpenMcdf3/CfbBinaryWriter.cs | 6 ----- OpenMcdf3/Fat.cs | 44 ++++++++++++++++++++++++++------ OpenMcdf3/FatSectorEnumerator.cs | 2 ++ OpenMcdf3/IOContext.cs | 1 + OpenMcdf3/TransactedStream.cs | 2 +- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index f17a0339..73e65954 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -24,12 +24,6 @@ public long Position public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); - public override void Write(byte value) - { - Span localBuffer = stackalloc byte[1] { value }; - Write(localBuffer); - } - #endif public void Write(in Guid value) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 48993745..70baf9d2 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System.Buffers.Binary; +using System.Collections; using System.Diagnostics; namespace OpenMcdf3; @@ -13,6 +14,8 @@ internal sealed class Fat : IEnumerable, IDisposable private readonly int DifatArrayElementCount; private readonly int FatElementsPerSector; private readonly int DifatElementsPerSector; + private readonly byte[] sector; + private bool isDirty; public Fat(IOContext ioContext) { @@ -21,10 +24,13 @@ public Fat(IOContext ioContext) DifatElementsPerSector = FatElementsPerSector - 1; DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; fatSectorEnumerator = new(ioContext); + sector = new byte[ioContext.SectorSize]; } public void Dispose() { + Flush(); + fatSectorEnumerator.Dispose(); } @@ -51,10 +57,33 @@ uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) return (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); } + public void Flush() + { + if (isDirty) + { + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatSectorEnumerator.Current.Position; + writer.Write(sector); + isDirty = false; + } + } + bool TryMoveToSectorForKey(uint key, out long offset) { uint sectorId = GetSectorIndexAndElementOffset(key, out offset); - return fatSectorEnumerator.MoveTo(sectorId); + if (fatSectorEnumerator.IsAt(sectorId)) + return true; + + Flush(); + + bool ok = fatSectorEnumerator.MoveTo(sectorId); + if (!ok) + return false; + + CfbBinaryReader reader = ioContext.Reader; + reader.Position = fatSectorEnumerator.Current.Position; + reader.Read(sector); + return true; } public bool TryGetValue(uint key, out uint value) @@ -68,9 +97,8 @@ public bool TryGetValue(uint key, out uint value) return false; } - CfbBinaryReader reader = ioContext.Reader; - reader.Position = fatSectorEnumerator.Current.Position + (elementIndex * sizeof(uint)); - value = reader.ReadUInt32(); + ReadOnlySpan slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + value = BinaryPrimitives.ReadUInt32LittleEndian(slice); return true; } @@ -81,9 +109,9 @@ public bool TrySetValue(uint key, uint value) if (!TryMoveToSectorForKey(key, out long elementIndex)) return false; - CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatSectorEnumerator.Current.Position + (elementIndex * sizeof(uint)); - writer.Write(value); + Span slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + BinaryPrimitives.WriteUInt32LittleEndian(slice, value); + isDirty = true; return true; } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index ce78326f..e1a4c66a 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -74,6 +74,8 @@ public bool MoveNext() return true; } + public bool IsAt(uint index) => !start && index == this.index; + /// /// Moves the enumerator to the specified sector. /// diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 72111bc6..a58e09ca 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -148,6 +148,7 @@ public void Commit() throw new InvalidOperationException("Cannot commit non-transacted storage."); WriteHeader(); + Fat.Flush(); transactedStream.Commit(); } public void Revert() diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 6af5179c..478e3259 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -87,7 +87,7 @@ public override void Write(byte[] buffer, int offset, int count) added = true; } - if (added && originalStream.Position < originalStream.Length && localCount != ioContext.SectorSize) + if (added && localCount != ioContext.SectorSize && originalStream.Position < originalStream.Length) { // Copy the existing sector data long originalPosition = originalStream.Position; From dc11f6cc703610cca04bdea1c76399745249a36a Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 23:03:28 +1300 Subject: [PATCH 053/114] Optimize SetLength --- OpenMcdf3/TransactedStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 478e3259..53377df0 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -99,10 +99,10 @@ public override void Write(byte[] buffer, int offset, int count) overlayStream.Write(this.buffer, 0, this.buffer.Length); } - if (overlayStream.Length < overlayPosition + ioContext.SectorSize) - overlayStream.SetLength(overlayPosition + ioContext.SectorSize); overlayStream.Position = overlayPosition + sectorOffset; overlayStream.Write(buffer, offset, localCount); + if (overlayStream.Length < overlayPosition + ioContext.SectorSize) + overlayStream.SetLength(overlayPosition + ioContext.SectorSize); originalStream.Seek(localCount, SeekOrigin.Current); } From f4f548b51696c5fa5f4c01b75d7bb9d7d08387d6 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 23:18:42 +1300 Subject: [PATCH 054/114] Benchmark improvement --- OpenMcdf3.Benchmarks/InMemory.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index 3dbe73d6..90ca4db4 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -12,13 +12,14 @@ namespace OpenMcdf3.Benchmark; [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class InMemory : IDisposable { + bool inMemory = false; private const int Kb = 1024; private const int Mb = Kb * Kb; private const string storageName = "MyStorage"; private const string streamName = "MyStream"; - private FileStream readStream = File.Create(Path.GetTempFileName()); - private FileStream writeStream = File.Create(Path.GetTempFileName()); + private Stream? readStream; + private Stream? writeStream; private byte[] buffer = Array.Empty(); @@ -38,8 +39,8 @@ public void Dispose() public void GlobalSetup() { buffer = new byte[BufferSize]; - //readStream = new MemoryStream(2 * TotalStreamSize); - //writeStream = new MemoryStream(2 * TotalStreamSize); + readStream = inMemory ? new MemoryStream(2 * TotalStreamSize) : File.Create(Path.GetTempFileName()); + writeStream = inMemory ? new MemoryStream(2 * TotalStreamSize) : File.Create(Path.GetTempFileName()); using var storage = RootStorage.Create(readStream, Version.V3, StorageModeFlags.LeaveOpen); using CfbStream stream = storage.CreateStream(streamName); From d0d86b02d2ef41f76267d9ab70d8525ce0033c1b Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 23:19:09 +1300 Subject: [PATCH 055/114] Optimize adding new FAT sector --- OpenMcdf3/FatSectorEnumerator.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index e1a4c66a..ec72ce59 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; namespace OpenMcdf3; @@ -145,10 +146,10 @@ public uint Add() { // No FAT sectors are free, so add a new one Header header = ioContext.Header; - (uint lastIndex, Sector lastSector) = MoveToEnd(); - uint nextIndex = lastIndex + 1; - long id = Math.Max(0, (ioContext.Reader.BaseStream.Length - ioContext.SectorSize) / ioContext.SectorSize); // TODO: Check - Sector newSector = new((uint)id, ioContext.SectorSize); + uint nextIndex = ioContext.Header.FatSectorCount + ioContext.Header.DifatSectorCount; + uint lastIndex = nextIndex - 1; + uint id = (uint)Math.Max(0, (ioContext.Reader.BaseStream.Length - ioContext.SectorSize) / ioContext.SectorSize); // TODO: Check + Sector newSector = new(id, ioContext.SectorSize); CfbBinaryWriter writer = ioContext.Writer; writer.Position = newSector.Position; @@ -163,20 +164,30 @@ public uint Add() header.Difat[nextIndex] = newSector.Id; header.FatSectorCount++; // TODO: Check + + writer.Position = newSector.Position; + writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); } else { + bool ok = MoveTo(lastIndex); + Debug.Assert(ok); + + Sector lastSector = current; + writer.Position = lastSector.EndPosition - sizeof(uint); + writer.Write(newSector.Id); + index = nextIndex; current = newSector; difatSectorId = newSector.Id; sectorType = SectorType.Difat; + writer.Position = newSector.Position; + writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); + writer.Position = newSector.EndPosition - sizeof(uint); writer.Write(SectorType.EndOfChain); - writer.Position = lastSector.EndPosition - sizeof(uint); - writer.Write(newSector.Id); - // Chain the sector if (header.FirstDifatSectorId == SectorType.EndOfChain) header.FirstDifatSectorId = newSector.Id; From 8fb7b948700bf4b8d13c1859384b33d78452b765 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 10:31:20 +1300 Subject: [PATCH 056/114] Simplify extending base stream --- OpenMcdf3/FatSectorEnumerator.cs | 3 +-- OpenMcdf3/IOContext.cs | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index ec72ce59..2e5b625f 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -148,8 +148,7 @@ public uint Add() Header header = ioContext.Header; uint nextIndex = ioContext.Header.FatSectorCount + ioContext.Header.DifatSectorCount; uint lastIndex = nextIndex - 1; - uint id = (uint)Math.Max(0, (ioContext.Reader.BaseStream.Length - ioContext.SectorSize) / ioContext.SectorSize); // TODO: Check - Sector newSector = new(id, ioContext.SectorSize); + Sector newSector = new(ioContext.SectorCount, ioContext.SectorSize); CfbBinaryWriter writer = ioContext.Writer; writer.Position = newSector.Position; diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index a58e09ca..10ef2f89 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -68,6 +68,10 @@ public FatStream MiniStream public Version Version => (Version)Header.MajorVersion; + public long Length => Reader.BaseStream.Length; + + public uint SectorCount => (uint)Math.Max(0, (Length - SectorSize) / SectorSize); // TODO: Check + public IOContext(Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) { this.stream = stream; @@ -80,7 +84,7 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I Stream transactedStream = stream; if (contextFlags.HasFlag(IOContextFlags.Transacted)) { - Stream overlayStream = stream is MemoryStream ? new MemoryStream((int)stream.Length) : File.Create(Path.GetTempFileName()); + Stream overlayStream = stream is MemoryStream ? new MemoryStream() : File.Create(Path.GetTempFileName()); transactedStream = new TransactedStream(this, stream, overlayStream); } @@ -151,6 +155,7 @@ public void Commit() Fat.Flush(); transactedStream.Commit(); } + public void Revert() { if (writer is null || writer.BaseStream is not TransactedStream transactedStream) From 41aee2ac610f160b896f09a87432c10505a89ea7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 5 Nov 2024 23:32:06 +1300 Subject: [PATCH 057/114] Improve chain extension --- OpenMcdf3/FatChainEnumerator.cs | 35 +++++++++++++++++++-------------- OpenMcdf3/FatStream.cs | 19 ++++++++++++------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index 2d538656..cc794d04 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -133,21 +133,7 @@ public long GetLength() /// The ID of the new sector public uint Extend() { - if (startId == SectorType.EndOfChain) - { - startId = ioContext.Fat.Add(fatEnumerator, 0); - return startId; - } - - uint lastId = startId; - while (MoveNext()) - { - lastId = current.Value; - } - - uint id = ioContext.Fat.Add(fatEnumerator, lastId); - ioContext.Fat[lastId] = id; - return id; + return ExtendFrom(0); } /// @@ -182,6 +168,25 @@ public void Extend(uint requiredChainLength) length = requiredChainLength; } + public uint ExtendFrom(uint hintId) + { + if (startId == SectorType.EndOfChain) + { + startId = ioContext.Fat.Add(fatEnumerator, hintId); + return startId; + } + + uint lastId = startId; + while (MoveNext()) + { + lastId = current.Value; + } + + uint id = ioContext.Fat.Add(fatEnumerator, lastId); + ioContext.Fat[lastId] = id; + return id; + } + public void Shrink(uint requiredChainLength) { uint chainLength = (uint)GetLength(); diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index ad98aed5..ef711519 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -166,23 +166,28 @@ public override void Write(byte[] buffer, int offset, int count) if (count == 0) return; - if (position + count > ChainCapacity) - SetLength(position + count); + //if (position + count > ChainCapacity) + // SetLength(position + count); uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); - if (!chain.MoveTo(chainIndex)) - throw new InvalidOperationException($"Failed to move to FAT chain index: {chainIndex}"); CfbBinaryWriter writer = ioContext.Writer; int writeCount = 0; - do + uint lastIndex = 0; + for (; ; ) { + if (!chain.MoveTo(chainIndex)) + { + lastIndex = chain.ExtendFrom(lastIndex); + } + Sector sector = chain.CurrentSector; writer.Position = sector.Position + sectorOffset; int remaining = count - writeCount; int localOffset = offset + writeCount; long writeLength = Math.Min(remaining, sector.Length - sectorOffset); writer.Write(buffer, localOffset, (int)writeLength); + ioContext.ExtendStreamLength(sector.EndPosition); position += writeLength; writeCount += (int)writeLength; if (position > Length) @@ -190,7 +195,9 @@ public override void Write(byte[] buffer, int offset, int count) sectorOffset = 0; if (writeCount >= count) return; - } while (chain.MoveNext()); + + chainIndex++; + } throw new InvalidOperationException($"End of FAT chain was reached"); } From a35e3716f5cc268c94c7a42c962bebde5360803e Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 11:21:21 +1300 Subject: [PATCH 058/114] Optimize transacted stream --- OpenMcdf3/TransactedStream.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 53377df0..2391f9e0 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -62,10 +62,7 @@ public override int Read(byte[] buffer, int offset, int count) return read; } - public override long Seek(long offset, SeekOrigin origin) - { - return originalStream.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) => originalStream.Seek(offset, origin); public override void SetLength(long value) => overlayStream.SetLength(value); @@ -87,13 +84,12 @@ public override void Write(byte[] buffer, int offset, int count) added = true; } - if (added && localCount != ioContext.SectorSize && originalStream.Position < originalStream.Length) + long originalPosition = originalStream.Position; + if (added && localCount != ioContext.SectorSize && originalPosition < originalStream.Length) { // Copy the existing sector data - long originalPosition = originalStream.Position; originalStream.Position = originalPosition - sectorOffset; originalStream.ReadExactly(this.buffer); - originalStream.Position = originalPosition; overlayStream.Position = overlayPosition; overlayStream.Write(this.buffer, 0, this.buffer.Length); @@ -101,9 +97,7 @@ public override void Write(byte[] buffer, int offset, int count) overlayStream.Position = overlayPosition + sectorOffset; overlayStream.Write(buffer, offset, localCount); - if (overlayStream.Length < overlayPosition + ioContext.SectorSize) - overlayStream.SetLength(overlayPosition + ioContext.SectorSize); - originalStream.Seek(localCount, SeekOrigin.Current); + originalStream.Position = originalPosition + localCount; } public void Commit() From 35ff3907659cbff1048e55ba0eb585bb597f642c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 11:22:29 +1300 Subject: [PATCH 059/114] Flush mini stream --- OpenMcdf3/IOContext.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 10ef2f89..d1cef369 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -151,8 +151,9 @@ public void Commit() if (writer is null || writer.BaseStream is not TransactedStream transactedStream) throw new InvalidOperationException("Cannot commit non-transacted storage."); - WriteHeader(); + miniStream?.Flush(); Fat.Flush(); + WriteHeader(); transactedStream.Commit(); } From ad77175f7a102c4009ab9c95b52d8e5da9d0be42 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 12:08:20 +1300 Subject: [PATCH 060/114] Fix transaction stream length --- OpenMcdf3/IOContext.cs | 13 +++++++++---- OpenMcdf3/TransactedStream.cs | 15 ++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index d1cef369..d9dae135 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -68,7 +68,7 @@ public FatStream MiniStream public Version Version => (Version)Header.MajorVersion; - public long Length => Reader.BaseStream.Length; + public long Length { get; private set; } public uint SectorCount => (uint)Math.Max(0, (Length - SectorSize) / SectorSize); // TODO: Check @@ -80,6 +80,7 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I Header = contextFlags.HasFlag(IOContextFlags.Create) ? new(version) : reader.ReadHeader(); SectorSize = 1 << Header.SectorShift; MiniSectorSize = 1 << Header.MiniSectorShift; + Length = stream.Length; Stream transactedStream = stream; if (contextFlags.HasFlag(IOContextFlags.Transacted)) @@ -116,7 +117,12 @@ public void Dispose() if (!IsDisposed) { if (writer is not null && writer.BaseStream is not TransactedStream) + { + // Ensure the stream is as long as expected + writer.BaseStream.SetLength(Length); WriteHeader(); + } + miniStream?.Dispose(); miniFat?.Dispose(); directoryEnumerator.Dispose(); @@ -129,9 +135,8 @@ public void Dispose() public void ExtendStreamLength(long length) { - Stream baseStream = Writer.BaseStream; - if (baseStream.Length < length) - baseStream.SetLength(length); + if (Length < length) + Length = length; } public void WriteHeader() diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 2391f9e0..2736e250 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -64,7 +64,7 @@ public override int Read(byte[] buffer, int offset, int count) public override long Seek(long offset, SeekOrigin origin) => originalStream.Seek(offset, origin); - public override void SetLength(long value) => overlayStream.SetLength(value); + public override void SetLength(long value) => throw new NotImplementedException(); public override void Write(byte[] buffer, int offset, int count) { @@ -97,6 +97,8 @@ public override void Write(byte[] buffer, int offset, int count) overlayStream.Position = overlayPosition + sectorOffset; overlayStream.Write(buffer, offset, localCount); + if (overlayStream.Length < overlayPosition + ioContext.SectorSize) + overlayStream.SetLength(overlayPosition + ioContext.SectorSize); originalStream.Position = originalPosition + localCount; } @@ -165,23 +167,22 @@ public override void Write(ReadOnlySpan buffer) added = true; } - if (added && originalStream.Position < originalStream.Length && localCount != ioContext.SectorSize) + long originalPosition = originalStream.Position; + if (added && localCount != ioContext.SectorSize && originalPosition < originalStream.Length) { // Copy the existing sector data - long originalPosition = originalStream.Position; originalStream.Position = originalPosition - sectorOffset; originalStream.ReadExactly(this.buffer); - originalStream.Position = originalPosition; overlayStream.Position = overlayPosition; overlayStream.Write(this.buffer, 0, this.buffer.Length); } - if (overlayStream.Length < overlayPosition + ioContext.SectorSize) - overlayStream.SetLength(overlayPosition + ioContext.SectorSize); overlayStream.Position = overlayPosition + sectorOffset; overlayStream.Write(buffer); - originalStream.Seek(localCount, SeekOrigin.Current); + if (overlayStream.Length < overlayPosition + ioContext.SectorSize) + overlayStream.SetLength(overlayPosition + ioContext.SectorSize); + originalStream.Position = originalPosition + localCount; } #endif From cb549bd5b2077667f593fcfe72bc5ad6d6c09680 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 12:54:15 +1300 Subject: [PATCH 061/114] Cache the current mini FAT sector --- OpenMcdf3/FatChainEnumerator.cs | 2 + OpenMcdf3/IOContext.cs | 1 + OpenMcdf3/MiniFat.cs | 73 +++++++++++++++++++++++++-------- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index cc794d04..18c89890 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -47,6 +47,8 @@ public FatChainEntry Current /// object IEnumerator.Current => Current; + public bool IsAt(uint index) => !start && index == this.index; + /// public bool MoveNext() { diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index d9dae135..f45d63cb 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -157,6 +157,7 @@ public void Commit() throw new InvalidOperationException("Cannot commit non-transacted storage."); miniStream?.Flush(); + miniFat?.Flush(); Fat.Flush(); WriteHeader(); transactedStream.Commit(); diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index 5fe425a0..e2a526f4 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System.Buffers.Binary; +using System.Collections; using System.Diagnostics; namespace OpenMcdf3; @@ -11,15 +12,34 @@ internal sealed class MiniFat : IEnumerable, IDisposable private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; private readonly int ElementsPerSector; + private readonly byte[] sector; + private bool isDirty; public MiniFat(IOContext ioContext) { this.ioContext = ioContext; ElementsPerSector = ioContext.SectorSize / sizeof(uint); fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); + sector = new byte[ioContext.SectorSize]; } - public void Dispose() => fatChainEnumerator.Dispose(); + public void Dispose() + { + Flush(); + + fatChainEnumerator.Dispose(); + } + + public void Flush() + { + if (isDirty) + { + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatChainEnumerator.CurrentSector.Position; + writer.Write(sector); + isDirty = false; + } + } public IEnumerator GetEnumerator() => new MiniFatEnumerator(ioContext); @@ -35,33 +55,54 @@ public uint this[uint key] } set { - ThrowHelper.ThrowIfSectorIdIsInvalid(key); + if (!TrySetValue(key, value)) + throw new KeyNotFoundException($"Mini FAT index not found: {key}."); + } + } - uint fatSectorIndex = (uint)Math.DivRem(key, ElementsPerSector, out long elementIndex); - if (!fatChainEnumerator.MoveTo(fatSectorIndex)) - throw new KeyNotFoundException($"Mini FAT index not found: {fatSectorIndex}."); + bool TryMoveToSectorForKey(uint key, out long elementIndex) + { + uint fatChain = (uint)Math.DivRem(key, ElementsPerSector, out elementIndex); + if (fatChainEnumerator.IsAt(fatChain)) + return true; - CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatChainEnumerator.CurrentSector.Position + (elementIndex * sizeof(uint)); - writer.Write(value); - } + Flush(); + + bool ok = fatChainEnumerator.MoveTo(fatChain); + if (!ok) + return false; + + CfbBinaryReader reader = ioContext.Reader; + reader.Position = fatChainEnumerator.CurrentSector.Position; + reader.Read(sector); + return true; } public bool TryGetValue(uint key, out uint value) { ThrowHelper.ThrowIfSectorIdIsInvalid(key); - uint fatSectorIndex = (uint)Math.DivRem(key, ElementsPerSector, out long elementIndex); - bool ok = fatChainEnumerator.MoveTo(fatSectorIndex); - if (!ok) + if (!TryMoveToSectorForKey(key, out long elementIndex)) { value = uint.MaxValue; return false; } - CfbBinaryReader reader = ioContext.Reader; - reader.Position = fatChainEnumerator.CurrentSector.Position + (elementIndex * sizeof(uint)); - value = reader.ReadUInt32(); + Span slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + value = BinaryPrimitives.ReadUInt32LittleEndian(slice); + return true; + } + + public bool TrySetValue(uint key, uint value) + { + ThrowHelper.ThrowIfSectorIdIsInvalid(key); + + if (!TryMoveToSectorForKey(key, out long elementIndex)) + return false; + + Span slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + BinaryPrimitives.WriteUInt32LittleEndian(slice, value); + isDirty = true; return true; } From ea2d4bf5c79d5e919f29e1af9fde493e51d502d5 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 13:25:08 +1300 Subject: [PATCH 062/114] Benchmark improvements --- OpenMcdf3.Perf/Program.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/OpenMcdf3.Perf/Program.cs b/OpenMcdf3.Perf/Program.cs index 2e356c9a..751cf9b1 100644 --- a/OpenMcdf3.Perf/Program.cs +++ b/OpenMcdf3.Perf/Program.cs @@ -7,29 +7,31 @@ internal sealed class Program static void Main(string[] args) { var stopwatch = Stopwatch.StartNew(); - Write(Version.V3, 4096, 1024 / 4, StorageModeFlags.Transacted); + Write(Version.V3, StorageModeFlags.Transacted, 1024 * 1024, 1024 * 1024, 100); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); } - static void Write(Version version, int length, int iterations, StorageModeFlags storageModeFlags) + static void Write(Version version, StorageModeFlags storageModeFlags, int bufferLength, int streamLength, int iterations) { // Fill with bytes equal to their position modulo 256 - byte[] expectedBuffer = new byte[length]; - for (int i = 0; i < length; i++) + byte[] expectedBuffer = new byte[bufferLength]; + for (int i = 0; i < bufferLength; i++) expectedBuffer[i] = (byte)i; //byte[] actualBuffer = new byte[length]; - using MemoryStream memoryStream = new(2 * length * iterations); - using var rootStorage = RootStorage.Create(memoryStream, version, storageModeFlags); - using Stream stream = rootStorage.CreateStream("TestStream"); - + //using MemoryStream memoryStream = new(2 * length * iterations); + using FileStream baseStream = File.Create(Path.GetTempFileName()); for (int i = 0; i < iterations; i++) { - stream.Write(expectedBuffer, 0, expectedBuffer.Length); - } + using var rootStorage = RootStorage.Create(baseStream, version, storageModeFlags); + using Stream stream = rootStorage.CreateStream("TestStream"); - if (storageModeFlags.HasFlag(StorageModeFlags.Transacted)) - rootStorage.Commit(); + for (int j = 0; j < streamLength / bufferLength; j++) + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + + if (storageModeFlags.HasFlag(StorageModeFlags.Transacted)) + rootStorage.Commit(); + } } } From 340bd2b1a9c1826381f7c31a694739efbc26a1d2 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 13:26:08 +1300 Subject: [PATCH 063/114] Improve dotnet 8 FatStream write --- OpenMcdf3/FatStream.cs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index ef711519..7923be07 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -161,14 +161,12 @@ public override void Write(byte[] buffer, int offset, int count) { ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); + this.ThrowIfDisposed(disposed); this.ThrowIfNotWritable(); if (count == 0) return; - //if (position + count > ChainCapacity) - // SetLength(position + count); - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); CfbBinaryWriter writer = ioContext.Writer; @@ -177,9 +175,7 @@ public override void Write(byte[] buffer, int offset, int count) for (; ; ) { if (!chain.MoveTo(chainIndex)) - { lastIndex = chain.ExtendFrom(lastIndex); - } Sector sector = chain.CurrentSector; writer.Position = sector.Position + sectorOffset; @@ -248,22 +244,22 @@ public override int Read(Span buffer) public override void Write(ReadOnlySpan buffer) { + this.ThrowIfDisposed(disposed); this.ThrowIfNotWritable(); if (buffer.Length == 0) return; - if (position + buffer.Length > ChainCapacity) - SetLength(position + buffer.Length); - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); - if (!chain.MoveTo(chainIndex)) - throw new InvalidOperationException($"Failed to move to FAT chain index: {chainIndex}"); CfbBinaryWriter writer = ioContext.Writer; int writeCount = 0; - do + uint lastIndex = 0; + for (; ; ) { + if (!chain.MoveTo(chainIndex)) + lastIndex = chain.ExtendFrom(lastIndex); + Sector sector = chain.CurrentSector; writer.Position = sector.Position + sectorOffset; int remaining = buffer.Length - writeCount; @@ -271,6 +267,7 @@ public override void Write(ReadOnlySpan buffer) long writeLength = Math.Min(remaining, sector.Length - sectorOffset); ReadOnlySpan slice = buffer.Slice(localOffset, (int)writeLength); writer.Write(slice); + ioContext.ExtendStreamLength(sector.EndPosition); position += writeLength; writeCount += (int)writeLength; if (position > Length) @@ -278,7 +275,9 @@ public override void Write(ReadOnlySpan buffer) sectorOffset = 0; if (writeCount >= buffer.Length) return; - } while (chain.MoveNext()); + + chainIndex++; + } throw new InvalidOperationException($"End of FAT chain was reached"); } From f2f6ac2110468e366242a03a249f4bbc78e9c01e Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 14:56:30 +1300 Subject: [PATCH 064/114] Add transacted read-only test --- OpenMcdf3.Tests/StreamTests.cs | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 058182ab..bbf9d460 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -397,6 +397,59 @@ public void ModifyCommit(Version version, int length) } } + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void TransactedRead(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + using CfbStream stream = rootStorage.CreateStream("TestStream1"); + Assert.AreEqual(0, stream.Length); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + } + + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.Transacted)) + { + using CfbStream stream = rootStorage.OpenStream("TestStream1"); + byte[] actualBuffer = new byte[length]; + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + } + [TestMethod] [DataRow(Version.V3, 0)] [DataRow(Version.V3, 63)] From 375f1a0aa980ba1823f97cf9b68546f67e7ed55a Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 14:56:52 +1300 Subject: [PATCH 065/114] Only write directories when dirty --- OpenMcdf3/FatChainEnumerator.cs | 2 ++ OpenMcdf3/FatStream.cs | 12 ++++++++++-- OpenMcdf3/MiniFatStream.cs | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index 18c89890..f97128a3 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -237,4 +237,6 @@ public void Reset(uint startSectorId) index = uint.MaxValue; current = FatChainEntry.Invalid; } + + public override string ToString() => $"{current}"; } diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 7923be07..06f6aefe 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -10,6 +10,7 @@ internal class FatStream : Stream readonly IOContext ioContext; readonly FatChainEnumerator chain; long position; + bool isDirty; bool disposed; internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) @@ -62,11 +63,14 @@ public override void Flush() { this.ThrowIfDisposed(disposed); - if (CanWrite) + if (isDirty) { ioContext.Write(DirectoryEntry); - ioContext.Writer!.Flush(); + isDirty = false; } + + if (CanWrite) + ioContext.Writer!.Flush(); } /// @@ -154,6 +158,7 @@ public override void SetLength(long value) DirectoryEntry.StartSectorId = chain.StartId; DirectoryEntry.StreamLength = value; + isDirty = true; } /// @@ -187,7 +192,10 @@ public override void Write(byte[] buffer, int offset, int count) position += writeLength; writeCount += (int)writeLength; if (position > Length) + { DirectoryEntry.StreamLength = position; + isDirty = true; + } sectorOffset = 0; if (writeCount >= count) return; diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index c7c00984..bddf78e6 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -9,6 +9,7 @@ internal sealed class MiniFatStream : Stream readonly MiniFatChainEnumerator miniChain; long position; bool disposed; + bool isDirty; internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) { @@ -52,8 +53,12 @@ public override void Flush() { this.ThrowIfDisposed(disposed); - if (CanWrite) + if (isDirty) + { ioContext.Write(DirectoryEntry); + isDirty = false; + } + ioContext.MiniStream.Flush(); } @@ -144,6 +149,7 @@ public override void SetLength(long value) DirectoryEntry.StartSectorId = miniChain.StartId; DirectoryEntry.StreamLength = value; + isDirty = true; } public override void Write(byte[] buffer, int offset, int count) @@ -177,7 +183,10 @@ public override void Write(byte[] buffer, int offset, int count) position += writeLength; writeCount += (int)writeLength; if (position > Length) + { DirectoryEntry.StreamLength = position; + isDirty = true; + } sectorOffset = 0; if (writeCount >= count) return; From 07c6b1476043dcb30820887e8b4886102972a65e Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 15:45:46 +1300 Subject: [PATCH 066/114] Buffer transacted streams --- OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 2 +- OpenMcdf3/IOContext.cs | 17 ++++++----- OpenMcdf3/OpenMcdf3.csproj | 2 +- OpenMcdf3/TransactedStream.cs | 40 +++++++++++++++----------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 1a72e048..68952b93 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -1,7 +1,7 @@ - net48;net8.0 + net8.0 Exe 11.0 enable diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index f45d63cb..15fa1cfc 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -16,6 +16,7 @@ internal sealed class IOContext : IDisposable readonly Stream stream; readonly DirectoryEntryEnumerator directoryEnumerator; readonly CfbBinaryWriter? writer; + TransactedStream? transactedStream; MiniFat? miniFat; FatStream? miniStream; @@ -82,16 +83,17 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I MiniSectorSize = 1 << Header.MiniSectorShift; Length = stream.Length; - Stream transactedStream = stream; + Stream actualStream = stream; if (contextFlags.HasFlag(IOContextFlags.Transacted)) { Stream overlayStream = stream is MemoryStream ? new MemoryStream() : File.Create(Path.GetTempFileName()); transactedStream = new TransactedStream(this, stream, overlayStream); + actualStream = new BufferedStream(transactedStream, SectorSize); } - Reader = new(transactedStream); + Reader = new(actualStream); if (stream.CanWrite) - writer = new(transactedStream); + writer = new(actualStream); Fat = new(this); directoryEnumerator = new(this); @@ -116,10 +118,10 @@ public void Dispose() { if (!IsDisposed) { - if (writer is not null && writer.BaseStream is not TransactedStream) + if (writer is not null && transactedStream is null) { // Ensure the stream is as long as expected - writer.BaseStream.SetLength(Length); + stream.SetLength(Length); WriteHeader(); } @@ -153,19 +155,20 @@ public void Write(DirectoryEntry entry) public void Commit() { - if (writer is null || writer.BaseStream is not TransactedStream transactedStream) + if (writer is null || transactedStream is null) throw new InvalidOperationException("Cannot commit non-transacted storage."); miniStream?.Flush(); miniFat?.Flush(); Fat.Flush(); WriteHeader(); + writer.BaseStream.Flush(); transactedStream.Commit(); } public void Revert() { - if (writer is null || writer.BaseStream is not TransactedStream transactedStream) + if (writer is null || transactedStream is null) throw new InvalidOperationException("Cannot commit non-transacted storage."); transactedStream.Revert(); diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj index 48b852fb..be7314e4 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net8.0 + net8.0 11.0 enable enable diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 2736e250..2c055837 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -42,24 +42,32 @@ public override int Read(byte[] buffer, int offset, int count) { ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); - uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); - int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; - int localCount = Math.Min(count, remainingFromSector); - Debug.Assert(localCount == count); - int read; - if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) - { - overlayStream.Position = overlayPosition + sectorOffset; - read = overlayStream.Read(buffer, offset, localCount); - originalStream.Seek(read, SeekOrigin.Current); - } - else + int totalRead = 0; + do { - read = originalStream.Read(buffer, offset, localCount); - } - - return read; + uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); + int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + int localCount = Math.Min(count - totalRead, remainingFromSector); + + if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) + { + overlayStream.Position = overlayPosition + sectorOffset; + read = overlayStream.Read(buffer, offset + totalRead, localCount); + originalStream.Seek(read, SeekOrigin.Current); + } + else + { + read = originalStream.Read(buffer, offset + totalRead, localCount); + } + + if (read == 0) + break; + + totalRead += read; + } while (totalRead < count); + + return totalRead; } public override long Seek(long offset, SeekOrigin origin) => originalStream.Seek(offset, origin); From a265ef1f9c004cb1e094a89597191c5df6bf4e38 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 16:25:34 +1300 Subject: [PATCH 067/114] Fix building for netstandard2.0 --- OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 2 +- OpenMcdf3/CfbBinaryReader.cs | 22 +++++++++++++++++++++- OpenMcdf3/OpenMcdf3.csproj | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 68952b93..1a72e048 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net48;net8.0 Exe 11.0 enable diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index fac360f1..c9c6b0f6 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Text; namespace OpenMcdf3; @@ -21,6 +22,25 @@ public long Position set => BaseStream.Position = value; } +#if NETSTANDARD2_0 + + public int Read(Span buffer) + { + byte[] array = ArrayPool.Shared.Rent(buffer.Length); + try + { + int bytesRead = BaseStream.Read(array, 0, buffer.Length); + array.AsSpan(0, bytesRead).CopyTo(buffer); + return bytesRead; + } + finally + { + ArrayPool.Shared.Return(array); + } + } + +#endif + public Guid ReadGuid() { int bytesRead = 0; diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj index be7314e4..48b852fb 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.0;net8.0 11.0 enable enable From 858eb99124aa64d52dd066a39618f4eb14c92053 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Wed, 6 Nov 2024 17:31:20 +1300 Subject: [PATCH 068/114] Delete directory entries --- OpenMcdf3.Tests/StorageTests.cs | 145 +++++++++++++++++++++++++++ OpenMcdf3/DirectoryEntry.cs | 2 + OpenMcdf3/DirectoryTreeEnumerator.cs | 47 ++++++++- OpenMcdf3/Storage.cs | 54 ++++++++-- 4 files changed, 237 insertions(+), 11 deletions(-) diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index a4c1ea81..003a4b87 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -51,4 +51,149 @@ public void CreateDuplicateStorageThrowsException(Version version) rootStorage.CreateStorage("Test"); Assert.ThrowsException(() => rootStorage.CreateStorage("Test")); } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteSingleStorage(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.CreateStorage("Test"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + rootStorage.Delete("Test"); + Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteRedBlackTreeChildLeaf(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.CreateStorage("Test1"); + rootStorage.CreateStorage("Test2"); + Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + rootStorage.Delete("Test1"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteRedBlackTreeSiblingLeaf(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.CreateStorage("Test1"); + rootStorage.CreateStorage("Test2"); + Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + rootStorage.Delete("Test2"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteRedBlackTreeSibling(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.CreateStorage("Test1"); + rootStorage.CreateStorage("Test2"); + rootStorage.CreateStorage("Test3"); + Assert.AreEqual(3, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + rootStorage.Delete("Test2"); + Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteStorageRecursively(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + Storage storage = rootStorage.CreateStorage("Test"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + + using CfbStream stream = storage.CreateStream("Test"); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + rootStorage.Delete("Test"); + Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Open(memoryStream)) + { + Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); + } + } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void DeleteStream(Version version) + { + using MemoryStream memoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.CreateStream("Test"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + } + + using (var rootStorage = RootStorage.Create(memoryStream, version)) + { + rootStorage.Delete("Test"); + Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); + } + } } diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 4a423d25..bcd6df4d 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -151,6 +151,8 @@ public bool Equals(DirectoryEntry? other) public void RecycleRoot() => Recycle(StorageType.Root, "Root Entry"); + public void Recycle() => Recycle(StorageType.Unallocated, string.Empty); + public void Recycle(StorageType storageType, string name) { Type = storageType; diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index c58262ec..8623589f 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -12,6 +12,7 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator private DirectoryEntry? child; private readonly Stack stack = new(); private readonly DirectoryEntryEnumerator directoryEntryEnumerator; + DirectoryEntry parent; DirectoryEntry? current; internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) @@ -20,6 +21,7 @@ internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) this.root = root; if (root.ChildId != StreamId.NoStream) child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); + parent = root; PushLeft(child); } @@ -49,10 +51,12 @@ public bool MoveNext() if (stack.Count == 0) { current = null; + parent = root; return false; } current = stack.Pop(); + parent = stack.Count == 0 ? root : stack.Peek(); if (current.RightSiblingId != StreamId.NoStream) { DirectoryEntry rightSibling = directoryEntryEnumerator.GetDictionaryEntry(current.RightSiblingId); @@ -66,6 +70,7 @@ public bool MoveNext() public void Reset() { current = null; + parent = root; stack.Clear(); PushLeft(child); } @@ -79,29 +84,29 @@ private void PushLeft(DirectoryEntry? node) } } - public bool MoveTo(StorageType type, string name) + public bool MoveTo(string name) { Reset(); while (MoveNext()) { - if (Current.Type == type && Current.Name == name) + if (Current.Name == name) return true; } return false; } - public DirectoryEntry? TryGetDirectoryEntry(StorageType type, string name) + public DirectoryEntry? TryGetDirectoryEntry(string name) { - if (MoveTo(type, name)) + if (MoveTo(name)) return Current; return null; } public DirectoryEntry Add(StorageType storageType, string name) { - if (MoveTo(storageType, name)) + if (MoveTo(name)) throw new IOException($"{storageType} \"{name}\" already exists."); DirectoryEntry entry = directoryEntryEnumerator.CreateOrRecycleDirectoryEntry(); @@ -116,6 +121,7 @@ void Add(DirectoryEntry entry) { Reset(); + // TODO: Implement balancing (all-black for now) entry.Color = NodeColor.Black; directoryEntryEnumerator.Write(entry); @@ -136,4 +142,35 @@ void Add(DirectoryEntry entry) directoryEntryEnumerator.Write(node); } } + + public void Remove(DirectoryEntry entry) + { + if (child is null) + throw new KeyNotFoundException("DirectoryEntry has no children"); + + if (root.ChildId == entry.Id) + { + root.ChildId = entry.LeftSiblingId; + directoryEntryEnumerator.Write(root); + if (root.ChildId == StreamId.NoStream) + child = null; + return; + } + + Reset(); + + while (MoveNext()) + { + if (current!.Id == entry.Id) + { + if (parent.LeftSiblingId == entry.Id) + parent.LeftSiblingId = entry.LeftSiblingId; + directoryEntryEnumerator.Write(parent); + + entry.Recycle(); + directoryEntryEnumerator.Write(entry); + break; + } + } + } } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index b3b3a347..e358c51b 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -11,6 +11,9 @@ public class Storage internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) { + if (directoryEntry.Type is not StorageType.Storage and not StorageType.Root) + throw new ArgumentException("DirectoryEntry must be a Storage or Root.", nameof(directoryEntry)); + this.ioContext = ioContext; DirectoryEntry = directoryEntry; } @@ -43,10 +46,10 @@ IEnumerable EnumerateDirectoryEntries() IEnumerable EnumerateDirectoryEntries(StorageType type) => EnumerateDirectoryEntries() .Where(e => e.Type == type); - DirectoryEntry? TryGetDirectoryEntry(StorageType storageType, string name) + DirectoryEntry? TryGetDirectoryEntry(string name) { using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); - return directoryTreeEnumerator.TryGetDirectoryEntry(storageType, name); + return directoryTreeEnumerator.TryGetDirectoryEntry(name); } DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) @@ -82,8 +85,9 @@ public Storage OpenStorage(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - DirectoryEntry entry = TryGetDirectoryEntry(StorageType.Storage, name) - ?? throw new DirectoryNotFoundException($"Storage not found: {name}."); + DirectoryEntry? entry = TryGetDirectoryEntry(name); + if (entry is null || entry.Type is not StorageType.Storage) + throw new DirectoryNotFoundException($"Storage not found: {name}."); return new Storage(ioContext, entry); } @@ -93,10 +97,48 @@ public CfbStream OpenStream(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - DirectoryEntry? entry = TryGetDirectoryEntry(StorageType.Stream, name) - ?? throw new FileNotFoundException($"Stream not found: {name}.", name); + DirectoryEntry? entry = TryGetDirectoryEntry(name); + if (entry is null || entry.Type is not StorageType.Stream) + throw new FileNotFoundException($"Stream not found: {name}.", name); // TODO: Return a Stream that can transition between FAT and mini FAT return new CfbStream(ioContext, entry); } + + public void Delete(string name) + { + ThrowHelper.ThrowIfNameIsInvalid(name); + + this.ThrowIfDisposed(ioContext.IsDisposed); + + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + DirectoryEntry? entry = directoryTreeEnumerator.TryGetDirectoryEntry(name); + if (entry is null) + return; + + if (entry.Type is StorageType.Storage && entry.ChildId is not StreamId.NoStream) + { + Storage storage = new(ioContext, entry); + foreach (EntryInfo childEntry in storage.EnumerateEntries()) + { + storage.Delete(childEntry.Name); + }; + } + + if (entry.Type is StorageType.Stream && entry.StartSectorId is not StreamId.NoStream) + { + if (entry.StreamLength < Header.MiniStreamCutoffSize) + { + using MiniFatChainEnumerator miniFatChainEnumerator = new(ioContext, entry.StartSectorId); + miniFatChainEnumerator.Shrink(0); + } + else + { + using FatChainEnumerator fatChainEnumerator = new(ioContext, entry.StartSectorId); + fatChainEnumerator.Shrink(0); + } + } + + directoryTreeEnumerator.Remove(entry); + } } From 0ba7e811dbd8408d8b49d9ccf0068302428b454c Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 09:56:03 +1300 Subject: [PATCH 069/114] Allow tracing directory entries --- OpenMcdf3.Tests/DebugWriter.cs | 2 ++ OpenMcdf3/DirectoryEntry.cs | 7 +++++++ OpenMcdf3/DirectoryTreeEnumerator.cs | 12 ++++++++++++ OpenMcdf3/Storage.cs | 6 ++++++ 4 files changed, 27 insertions(+) diff --git a/OpenMcdf3.Tests/DebugWriter.cs b/OpenMcdf3.Tests/DebugWriter.cs index f795da38..c1b66277 100644 --- a/OpenMcdf3.Tests/DebugWriter.cs +++ b/OpenMcdf3.Tests/DebugWriter.cs @@ -5,6 +5,8 @@ namespace OpenMcdf3.Tests; internal sealed class DebugWriter : TextWriter { + public static DebugWriter Default { get; } = new(); + public override Encoding Encoding => Encoding.Unicode; public override void Write(char value) => Debug.Write(value); diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index bcd6df4d..f08cf2ce 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -130,6 +130,13 @@ public DateTime ModifiedTime /// public long StreamLength { get; set; } + internal char ColorChar => Color switch + { + NodeColor.Red => 'R', + NodeColor.Black => 'B', + _ => '?' + }; + public override bool Equals(object? obj) => Equals(obj as DirectoryEntry); public bool Equals(DirectoryEntry? other) diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 8623589f..0101429c 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -173,4 +173,16 @@ public void Remove(DirectoryEntry entry) } } } + + internal void PrintTrace(TextWriter writer) + { + Reset(); + + while (MoveNext()) + { + for (int i = 0; i < stack.Count; i++) + writer.Write(" "); + writer.WriteLine($"{Current.ColorChar} {Current}"); + } + } } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index e358c51b..8c808a6d 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -141,4 +141,10 @@ public void Delete(string name) directoryTreeEnumerator.Remove(entry); } + + internal void TraceDirectoryEntries(TextWriter writer) + { + using DirectoryTreeEnumerator treeEnumerator = new(ioContext, DirectoryEntry); + treeEnumerator.PrintTrace(writer); + } } From ba9e15ccbae874a9ef57b9457cd2c9d8b26cbb25 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 10:59:16 +1300 Subject: [PATCH 070/114] Add directories class --- OpenMcdf3/Directories.cs | 95 +++++++++++++++++++++++++++ OpenMcdf3/DirectoryEntryEnumerator.cs | 88 ++----------------------- OpenMcdf3/DirectoryTreeEnumerator.cs | 29 ++++---- OpenMcdf3/FatStream.cs | 2 +- OpenMcdf3/IOContext.cs | 22 +++---- OpenMcdf3/MiniFatStream.cs | 2 +- OpenMcdf3/Storage.cs | 10 +-- 7 files changed, 131 insertions(+), 117 deletions(-) create mode 100644 OpenMcdf3/Directories.cs diff --git a/OpenMcdf3/Directories.cs b/OpenMcdf3/Directories.cs new file mode 100644 index 00000000..186043ab --- /dev/null +++ b/OpenMcdf3/Directories.cs @@ -0,0 +1,95 @@ +namespace OpenMcdf3; + +internal sealed class Directories : IDisposable +{ + private readonly IOContext ioContext; + private readonly FatChainEnumerator fatChainEnumerator; + private readonly DirectoryEntryEnumerator directoryEntryEnumerator; + private readonly int entriesPerSector; + + public Directories(IOContext ioContext) + { + this.ioContext = ioContext; + fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + directoryEntryEnumerator = new DirectoryEntryEnumerator(this); + entriesPerSector = ioContext.SectorSize / DirectoryEntry.Length; + } + + public void Dispose() + { + fatChainEnumerator.Dispose(); + } + + /// + /// Gets the for the specified stream ID. + /// + public DirectoryEntry GetDictionaryEntry(uint streamId) + { + if (!TryGetDictionaryEntry(streamId, out DirectoryEntry? entry)) + throw new KeyNotFoundException($"Directory entry {streamId} was not found."); + return entry!; + } + + public bool TryGetDictionaryEntry(uint streamId, out DirectoryEntry? entry) + { + if (streamId > StreamId.Maximum) + throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}.", nameof(streamId)); + + uint chainIndex = (uint)Math.DivRem(streamId, entriesPerSector, out long entryIndex); + if (!fatChainEnumerator.MoveTo(chainIndex)) + { + entry = null; + return false; + } + + ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); + entry = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, streamId); + return true; + } + + public DirectoryEntry CreateOrRecycleDirectoryEntry() + { + DirectoryEntry? entry = TryRecycleDirectoryEntry(); + if (entry is not null) + return entry; + + CfbBinaryWriter writer = ioContext.Writer; + uint id = fatChainEnumerator.Extend(); + if (ioContext.Header.FirstDirectorySectorId == SectorType.EndOfChain) + ioContext.Header.FirstDirectorySectorId = id; + + Sector sector = new(id, ioContext.SectorSize); + writer.Position = sector.Position; + for (int i = 0; i < entriesPerSector; i++) + writer.Write(DirectoryEntry.Unallocated); + + entry = TryRecycleDirectoryEntry() + ?? throw new InvalidOperationException("Failed to add or recycle directory entry."); + return entry; + } + + private DirectoryEntry? TryRecycleDirectoryEntry() + { + directoryEntryEnumerator.Reset(); + + while (directoryEntryEnumerator.MoveNext()) + { + DirectoryEntry current = directoryEntryEnumerator.Current; + if (directoryEntryEnumerator.Current.Type is StorageType.Unallocated) + return current; + } + + return null; + } + + public void Write(DirectoryEntry entry) + { + uint chainIndex = (uint)Math.DivRem(entry.Id, entriesPerSector, out long entryIndex); + if (!fatChainEnumerator.MoveTo(chainIndex)) + throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); + + CfbBinaryWriter writer = ioContext.Writer; + writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); + writer.Write(entry); + } +} diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 39f858a4..05880a6e 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -8,26 +8,21 @@ namespace OpenMcdf3; /// internal sealed class DirectoryEntryEnumerator : IEnumerator { - private readonly IOContext ioContext; - private readonly FatChainEnumerator fatChainEnumerator; + private readonly Directories directories; private bool start = true; private uint index = uint.MaxValue; private DirectoryEntry? current; - public DirectoryEntryEnumerator(IOContext ioContext) + public DirectoryEntryEnumerator(Directories directories) { - this.ioContext = ioContext; - fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + this.directories = directories; } /// public void Dispose() { - fatChainEnumerator.Dispose(); } - private int EntriesPerSector => ioContext.SectorSize / DirectoryEntry.Length; - /// public DirectoryEntry Current { @@ -48,92 +43,23 @@ public bool MoveNext() if (start) { start = false; - index = 0; + index = uint.MaxValue; } - uint chainIndex = (uint)Math.DivRem(index, EntriesPerSector, out long entryIndex); - if (!fatChainEnumerator.MoveTo(chainIndex)) + uint nextIndex = index + 1; + if (!directories.TryGetDictionaryEntry(nextIndex, out current)) { - current = null; index = uint.MaxValue; return false; } - ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); - current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, index); - index++; + index = nextIndex; return true; } - public DirectoryEntry CreateOrRecycleDirectoryEntry() - { - DirectoryEntry? entry = TryRecycleDirectoryEntry(); - if (entry is not null) - return entry; - - CfbBinaryWriter writer = ioContext.Writer; - uint id = fatChainEnumerator.Extend(); - if (ioContext.Header.FirstDirectorySectorId == SectorType.EndOfChain) - ioContext.Header.FirstDirectorySectorId = id; - - Sector sector = new(id, ioContext.SectorSize); - writer.Position = sector.Position; - int directoryEntriesPerSector = EntriesPerSector; - for (int i = 0; i < directoryEntriesPerSector; i++) - writer.Write(DirectoryEntry.Unallocated); - - entry = TryRecycleDirectoryEntry() - ?? throw new InvalidOperationException("Failed to add or recycle directory entry."); - return entry; - } - - private DirectoryEntry? TryRecycleDirectoryEntry() - { - Reset(); - - while (MoveNext()) - { - if (current!.Type == StorageType.Unallocated) - { - return current; - } - } - - return null; - } - - /// - /// Gets the for the specified stream ID. - /// - public DirectoryEntry GetDictionaryEntry(uint streamId) - { - if (streamId > StreamId.Maximum) - throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}.", nameof(streamId)); - - uint chainIndex = (uint)Math.DivRem(streamId, EntriesPerSector, out long entryIndex); - if (!fatChainEnumerator.MoveTo(chainIndex)) - throw new KeyNotFoundException($"Directory entry {streamId} was not found."); - - ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); - current = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, streamId); - return current; - } - - public void Write(DirectoryEntry entry) - { - uint chainIndex = (uint)Math.DivRem(entry.Id, EntriesPerSector, out long entryIndex); - if (!fatChainEnumerator.MoveTo(chainIndex)) - throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); - - CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); - writer.Write(entry); - } - /// public void Reset() { - fatChainEnumerator.Reset(); start = true; current = null; index = uint.MaxValue; diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 0101429c..8c840901 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -8,19 +8,19 @@ namespace OpenMcdf3; /// internal sealed class DirectoryTreeEnumerator : IEnumerator { + private readonly Directories directories; private readonly DirectoryEntry root; private DirectoryEntry? child; private readonly Stack stack = new(); - private readonly DirectoryEntryEnumerator directoryEntryEnumerator; DirectoryEntry parent; DirectoryEntry? current; - internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) + internal DirectoryTreeEnumerator(Directories directories, DirectoryEntry root) { - directoryEntryEnumerator = new(ioContext); + this.directories = directories; this.root = root; if (root.ChildId != StreamId.NoStream) - child = directoryEntryEnumerator.GetDictionaryEntry(root.ChildId); + child = directories.GetDictionaryEntry(root.ChildId); parent = root; PushLeft(child); } @@ -28,7 +28,6 @@ internal DirectoryTreeEnumerator(IOContext ioContext, DirectoryEntry root) /// public void Dispose() { - directoryEntryEnumerator.Dispose(); } /// @@ -59,7 +58,7 @@ public bool MoveNext() parent = stack.Count == 0 ? root : stack.Peek(); if (current.RightSiblingId != StreamId.NoStream) { - DirectoryEntry rightSibling = directoryEntryEnumerator.GetDictionaryEntry(current.RightSiblingId); + DirectoryEntry rightSibling = directories.GetDictionaryEntry(current.RightSiblingId); PushLeft(rightSibling); } @@ -80,7 +79,7 @@ private void PushLeft(DirectoryEntry? node) while (node is not null) { stack.Push(node); - node = node.LeftSiblingId == StreamId.NoStream ? null : directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); + node = node.LeftSiblingId == StreamId.NoStream ? null : directories.GetDictionaryEntry(node.LeftSiblingId); } } @@ -109,7 +108,7 @@ public DirectoryEntry Add(StorageType storageType, string name) if (MoveTo(name)) throw new IOException($"{storageType} \"{name}\" already exists."); - DirectoryEntry entry = directoryEntryEnumerator.CreateOrRecycleDirectoryEntry(); + DirectoryEntry entry = directories.CreateOrRecycleDirectoryEntry(); entry.Recycle(storageType, name); Add(entry); @@ -123,13 +122,13 @@ void Add(DirectoryEntry entry) // TODO: Implement balancing (all-black for now) entry.Color = NodeColor.Black; - directoryEntryEnumerator.Write(entry); + directories.Write(entry); if (root.ChildId == StreamId.NoStream) { Debug.Assert(child is null); root.ChildId = entry.Id; - directoryEntryEnumerator.Write(root); + directories.Write(root); child = entry; } else @@ -137,9 +136,9 @@ void Add(DirectoryEntry entry) Debug.Assert(child is not null); DirectoryEntry node = child!; while (node.LeftSiblingId != StreamId.NoStream) - node = directoryEntryEnumerator.GetDictionaryEntry(node.LeftSiblingId); + node = directories.GetDictionaryEntry(node.LeftSiblingId); node.LeftSiblingId = entry.Id; - directoryEntryEnumerator.Write(node); + directories.Write(node); } } @@ -151,7 +150,7 @@ public void Remove(DirectoryEntry entry) if (root.ChildId == entry.Id) { root.ChildId = entry.LeftSiblingId; - directoryEntryEnumerator.Write(root); + directories.Write(root); if (root.ChildId == StreamId.NoStream) child = null; return; @@ -165,10 +164,10 @@ public void Remove(DirectoryEntry entry) { if (parent.LeftSiblingId == entry.Id) parent.LeftSiblingId = entry.LeftSiblingId; - directoryEntryEnumerator.Write(parent); + directories.Write(parent); entry.Recycle(); - directoryEntryEnumerator.Write(entry); + directories.Write(entry); break; } } diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 06f6aefe..05fe2591 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -65,7 +65,7 @@ public override void Flush() if (isDirty) { - ioContext.Write(DirectoryEntry); + ioContext.Directories.Write(DirectoryEntry); isDirty = false; } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 15fa1cfc..3c13816b 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -14,9 +14,8 @@ enum IOContextFlags internal sealed class IOContext : IDisposable { readonly Stream stream; - readonly DirectoryEntryEnumerator directoryEnumerator; readonly CfbBinaryWriter? writer; - TransactedStream? transactedStream; + readonly TransactedStream? transactedStream; MiniFat? miniFat; FatStream? miniStream; @@ -36,6 +35,8 @@ public CfbBinaryWriter Writer public Fat Fat { get; } + public Directories Directories { get; } + public DirectoryEntry RootEntry { get; } public MiniFat MiniFat @@ -96,21 +97,19 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I writer = new(actualStream); Fat = new(this); - directoryEnumerator = new(this); + Directories = new(this); if (contextFlags.HasFlag(IOContextFlags.Create)) { - RootEntry = directoryEnumerator.CreateOrRecycleDirectoryEntry(); + RootEntry = Directories.CreateOrRecycleDirectoryEntry(); RootEntry.RecycleRoot(); WriteHeader(); - Write(RootEntry); + Directories.Write(RootEntry); } else { - if (!directoryEnumerator.MoveNext()) - throw new FormatException("Root directory entry not found."); - RootEntry = directoryEnumerator.Current; + RootEntry = Directories.GetDictionaryEntry(0); } } @@ -127,7 +126,7 @@ public void Dispose() miniStream?.Dispose(); miniFat?.Dispose(); - directoryEnumerator.Dispose(); + Directories.Dispose(); Fat.Dispose(); writer?.Dispose(); Reader.Dispose(); @@ -148,11 +147,6 @@ public void WriteHeader() writer.Write(Header); } - public void Write(DirectoryEntry entry) - { - directoryEnumerator.Write(entry); - } - public void Commit() { if (writer is null || transactedStream is null) diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index bddf78e6..cea8cdcf 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -55,7 +55,7 @@ public override void Flush() if (isDirty) { - ioContext.Write(DirectoryEntry); + ioContext.Directories.Write(DirectoryEntry); isDirty = false; } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 8c808a6d..4182f093 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -36,7 +36,7 @@ public IEnumerable EnumerateEntries(StorageType type) IEnumerable EnumerateDirectoryEntries() { - using DirectoryTreeEnumerator treeEnumerator = new(ioContext, DirectoryEntry); + using DirectoryTreeEnumerator treeEnumerator = new(ioContext.Directories, DirectoryEntry); while (treeEnumerator.MoveNext()) { yield return treeEnumerator.Current; @@ -48,13 +48,13 @@ IEnumerable EnumerateDirectoryEntries(StorageType type) => Enume DirectoryEntry? TryGetDirectoryEntry(string name) { - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); return directoryTreeEnumerator.TryGetDirectoryEntry(name); } DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) { - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); return directoryTreeEnumerator.Add(storageType, name); } @@ -111,7 +111,7 @@ public void Delete(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext, DirectoryEntry); + using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); DirectoryEntry? entry = directoryTreeEnumerator.TryGetDirectoryEntry(name); if (entry is null) return; @@ -144,7 +144,7 @@ public void Delete(string name) internal void TraceDirectoryEntries(TextWriter writer) { - using DirectoryTreeEnumerator treeEnumerator = new(ioContext, DirectoryEntry); + using DirectoryTreeEnumerator treeEnumerator = new(ioContext.Directories, DirectoryEntry); treeEnumerator.PrintTrace(writer); } } From cb689e7b9d5b91a79a7cbec338ae7fff0cb41329 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 11:53:31 +1300 Subject: [PATCH 071/114] Improve tracing --- OpenMcdf3.Tests/StreamTests.cs | 2 ++ OpenMcdf3/Fat.cs | 25 ++++++++++++++++++++++++- OpenMcdf3/FatEnumerator.cs | 29 ----------------------------- OpenMcdf3/MiniFat.cs | 6 +++++- OpenMcdf3/MiniFatEnumerator.cs | 10 ---------- 5 files changed, 31 insertions(+), 41 deletions(-) diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index bbf9d460..54580eec 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -132,6 +132,8 @@ public void Write(Version version, int length) Assert.AreEqual(length, stream.Length); Assert.AreEqual(length, stream.Position); + rootStorage.Trace(DebugWriter.Default); + byte[] actualBuffer = new byte[length]; stream.Position = 0; stream.ReadExactly(actualBuffer); diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 70baf9d2..3d7bf2c1 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -147,6 +147,29 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) internal void Trace(TextWriter writer) { using FatEnumerator fatEnumerator = new(ioContext); - fatEnumerator.Trace(writer); + + byte[] data = new byte[ioContext.SectorSize]; + + Stream baseStream = ioContext.Reader.BaseStream; + + writer.WriteLine("Start of FAT ================="); + + while (fatEnumerator.MoveNext()) + { + FatEntry current = fatEnumerator.Current; + if (current.IsFree) + { + writer.WriteLine($"{current}"); + } + else + { + baseStream.Position = fatEnumerator.CurrentSector.Position; + baseStream.ReadExactly(data, 0, data.Length); + string hex = BitConverter.ToString(data); + writer.WriteLine($"{current}: {hex}"); + } + } + + writer.WriteLine("End of FAT ==================="); } } diff --git a/OpenMcdf3/FatEnumerator.cs b/OpenMcdf3/FatEnumerator.cs index d362c6e2..d64aebad 100644 --- a/OpenMcdf3/FatEnumerator.cs +++ b/OpenMcdf3/FatEnumerator.cs @@ -99,34 +99,5 @@ public void Reset() value = uint.MaxValue; } - internal void Trace(TextWriter writer) - { - Reset(); - - byte[] data = new byte[ioContext.SectorSize]; - - Stream baseStream = ioContext.Reader.BaseStream; - - writer.WriteLine("Start of FAT ================="); - - while (MoveNext()) - { - FatEntry current = Current; - if (current.IsFree) - { - writer.WriteLine($"{current}"); - } - else - { - baseStream.Position = CurrentSector.Position; - baseStream.ReadExactly(data, 0, data.Length); - string hex = BitConverter.ToString(data); - writer.WriteLine($"{current}: {hex}"); - } - } - - writer.WriteLine("End of FAT ==================="); - } - public override string ToString() => $"{Current}"; } diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index e2a526f4..8c76ddf5 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -142,6 +142,10 @@ public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) internal void Trace(TextWriter writer) { using MiniFatEnumerator miniFatEnumerator = new(ioContext); - miniFatEnumerator.Trace(writer); + + writer.WriteLine("Start of Mini FAT ============"); + while (miniFatEnumerator.MoveNext()) + writer.WriteLine($"{miniFatEnumerator.Current}"); + writer.WriteLine("End of Mini FAT =============="); } } diff --git a/OpenMcdf3/MiniFatEnumerator.cs b/OpenMcdf3/MiniFatEnumerator.cs index 48c9e017..4d108a1d 100644 --- a/OpenMcdf3/MiniFatEnumerator.cs +++ b/OpenMcdf3/MiniFatEnumerator.cs @@ -103,14 +103,4 @@ public void Reset() index = uint.MaxValue; value = uint.MaxValue; } - - internal void Trace(TextWriter writer) - { - Reset(); - - writer.WriteLine("Start of Mini FAT ============"); - while (MoveNext()) - writer.WriteLine($"Mini FAT entry {Current}"); - writer.WriteLine("End of Mini FAT =============="); - } } From a066e5cf06726c00a03f4bf4b1e11711710aa0f5 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 13:39:20 +1300 Subject: [PATCH 072/114] Fix FAT sector caching when adding sector --- OpenMcdf3/Fat.cs | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 3d7bf2c1..e45946d2 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -14,7 +14,8 @@ internal sealed class Fat : IEnumerable, IDisposable private readonly int DifatArrayElementCount; private readonly int FatElementsPerSector; private readonly int DifatElementsPerSector; - private readonly byte[] sector; + private readonly byte[] cachedSectorBuffer; + Sector cachedSector = Sector.EndOfChain; private bool isDirty; public Fat(IOContext ioContext) @@ -24,7 +25,7 @@ public Fat(IOContext ioContext) DifatElementsPerSector = FatElementsPerSector - 1; DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; fatSectorEnumerator = new(ioContext); - sector = new byte[ioContext.SectorSize]; + cachedSectorBuffer = new byte[ioContext.SectorSize]; } public void Dispose() @@ -57,13 +58,27 @@ uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) return (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); } + void CacheCurrentSector() + { + Sector current = fatSectorEnumerator.Current; + if (cachedSector.Id == current.Id) + return; + + Flush(); + + CfbBinaryReader reader = ioContext.Reader; + reader.Position = current.Position; + reader.Read(cachedSectorBuffer); + cachedSector = current; + } + public void Flush() { if (isDirty) { CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatSectorEnumerator.Current.Position; - writer.Write(sector); + writer.Position = cachedSector.Position; + writer.Write(cachedSectorBuffer); isDirty = false; } } @@ -71,18 +86,11 @@ public void Flush() bool TryMoveToSectorForKey(uint key, out long offset) { uint sectorId = GetSectorIndexAndElementOffset(key, out offset); - if (fatSectorEnumerator.IsAt(sectorId)) - return true; - - Flush(); - bool ok = fatSectorEnumerator.MoveTo(sectorId); if (!ok) return false; - CfbBinaryReader reader = ioContext.Reader; - reader.Position = fatSectorEnumerator.Current.Position; - reader.Read(sector); + CacheCurrentSector(); return true; } @@ -97,7 +105,7 @@ public bool TryGetValue(uint key, out uint value) return false; } - ReadOnlySpan slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + ReadOnlySpan slice = cachedSectorBuffer.AsSpan((int)elementIndex * sizeof(uint)); value = BinaryPrimitives.ReadUInt32LittleEndian(slice); return true; } @@ -109,7 +117,7 @@ public bool TrySetValue(uint key, uint value) if (!TryMoveToSectorForKey(key, out long elementIndex)) return false; - Span slice = sector.AsSpan((int)elementIndex * sizeof(uint)); + Span slice = cachedSectorBuffer.AsSpan((int)elementIndex * sizeof(uint)); BinaryPrimitives.WriteUInt32LittleEndian(slice, value); isDirty = true; return true; @@ -127,11 +135,15 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) && fatEnumerator.MoveNextFreeEntry(); if (!movedToFreeEntry) { + Flush(); + uint newSectorId = fatSectorEnumerator.Add(); // Next id must be free bool ok = fatEnumerator.MoveTo(newSectorId + 1); Debug.Assert(ok); + + CacheCurrentSector(); } FatEntry entry = fatEnumerator.Current; From 3103c0deb98e3aace6c1af3f0a26dc049f5e95b1 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 14:18:26 +1300 Subject: [PATCH 073/114] Fix FAT sector index calculation --- OpenMcdf3/Fat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index e45946d2..afc359a8 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -55,7 +55,7 @@ uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) { if (key < DifatArrayElementCount) return (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); - return (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); + return Header.DifatArrayLength + (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); } void CacheCurrentSector() From 76b8dfce41a319aee6598f3d70c2cdf918d43bcd Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 14:27:55 +1300 Subject: [PATCH 074/114] Validate FAT in tests --- OpenMcdf3.Benchmarks/InMemory.cs | 2 +- OpenMcdf3.Tests/StreamTests.cs | 11 ++++++++++- OpenMcdf3/Fat.cs | 16 +++++++++++++++- OpenMcdf3/FatSectorEnumerator.cs | 1 - OpenMcdf3/MiniFat.cs | 14 ++++++++++++++ OpenMcdf3/RootStorage.cs | 8 +++++++- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs index 90ca4db4..209088ab 100644 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ b/OpenMcdf3.Benchmarks/InMemory.cs @@ -12,7 +12,7 @@ namespace OpenMcdf3.Benchmark; [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class InMemory : IDisposable { - bool inMemory = false; + bool inMemory = true; private const int Kb = 1024; private const int Mb = Kb * Kb; private const string storageName = "MyStorage"; diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 54580eec..717444a5 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -29,6 +29,8 @@ public void ReadViaCopyTo(Version version, int length) { string fileName = $"TestStream_v{(int)version}_{length}.cfs"; using var rootStorage = RootStorage.OpenRead(fileName); + rootStorage.Validate(); + using Stream stream = rootStorage.OpenStream("TestStream"); Assert.AreEqual(length, stream.Length); @@ -132,7 +134,7 @@ public void Write(Version version, int length) Assert.AreEqual(length, stream.Length); Assert.AreEqual(length, stream.Position); - rootStorage.Trace(DebugWriter.Default); + rootStorage.Validate(); byte[] actualBuffer = new byte[length]; stream.Position = 0; @@ -188,6 +190,7 @@ public void WriteThenRead(Version version, int length) using (var rootStorage = RootStorage.Open(memoryStream)) { using CfbStream stream = rootStorage.OpenStream("TestStream"); + rootStorage.Validate(); Assert.AreEqual(length, stream.Length); byte[] actualBuffer = new byte[length]; @@ -303,6 +306,8 @@ public void Modify(Version version, int length) using (var rootStorage = RootStorage.Open(memoryStream)) { + rootStorage.Validate(); + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) { Assert.AreEqual(length, stream.Length); @@ -379,6 +384,8 @@ public void ModifyCommit(Version version, int length) using (var rootStorage = RootStorage.Open(memoryStream)) { + rootStorage.Validate(); + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) { Assert.AreEqual(length, stream.Length); @@ -507,6 +514,8 @@ public void ModifyRevert(Version version, int length) using (var rootStorage = RootStorage.Open(memoryStream)) { + rootStorage.Validate(); + using (CfbStream stream = rootStorage.OpenStream("TestStream1")) { Assert.AreEqual(length, stream.Length); diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index afc359a8..adb5d6e5 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -156,7 +156,7 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - internal void Trace(TextWriter writer) + internal void WriteTrace(TextWriter writer) { using FatEnumerator fatEnumerator = new(ioContext); @@ -184,4 +184,18 @@ internal void Trace(TextWriter writer) writer.WriteLine("End of FAT ==================="); } + + internal void Validate() + { + using FatEnumerator fatEnumerator = new(ioContext); + + while (fatEnumerator.MoveNext()) + { + FatEntry current = fatEnumerator.Current; + if (current.Value <= SectorType.Maximum && fatEnumerator.CurrentSector.EndPosition > ioContext.Length) + { + throw new FormatException($"FAT entry {current} is beyond the end of the stream."); + } + } + } } diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 2e5b625f..3f2ea9bb 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -194,7 +194,6 @@ public uint Add() } ioContext.Fat[newSector.Id] = sectorType; - return newSector.Id; } } diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index 8c76ddf5..d804f43a 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -148,4 +148,18 @@ internal void Trace(TextWriter writer) writer.WriteLine($"{miniFatEnumerator.Current}"); writer.WriteLine("End of Mini FAT =============="); } + + internal void Validate() + { + using MiniFatEnumerator miniFatEnumerator = new(ioContext); + + while (miniFatEnumerator.MoveNext()) + { + FatEntry current = miniFatEnumerator.Current; + if (current.Value <= SectorType.Maximum && miniFatEnumerator.CurrentSector.EndPosition > ioContext.MiniStream.Length) + { + throw new FormatException($"Mini FAT entry {current} is beyond the end of the mini stream."); + } + } + } } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 0201bcef..fcdd90c2 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -89,7 +89,13 @@ public void Revert() internal void Trace(TextWriter writer) { writer.WriteLine(ioContext.Header); - ioContext.Fat.Trace(writer); + ioContext.Fat.WriteTrace(writer); ioContext.MiniFat.Trace(writer); } + + internal void Validate() + { + ioContext.Fat.Validate(); + ioContext.MiniFat.Validate(); + } } From b407f1786964236839fc37295531613da8275a13 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 13:48:27 +1300 Subject: [PATCH 075/114] Set Mini FAT sector count --- OpenMcdf3/FatSectorEnumerator.cs | 2 +- OpenMcdf3/MiniFat.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 3f2ea9bb..16b7c8ef 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -162,7 +162,7 @@ public uint Add() sectorType = SectorType.Fat; header.Difat[nextIndex] = newSector.Id; - header.FatSectorCount++; // TODO: Check + header.FatSectorCount++; writer.Position = newSector.Position; writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf3/MiniFat.cs index d804f43a..bbb22d6b 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf3/MiniFat.cs @@ -119,8 +119,10 @@ public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) writer.Position = sector.Position; writer.Write(SectorDataCache.GetFatEntryData(sector.Length)); - if (ioContext.Header.FirstMiniFatSectorId == SectorType.EndOfChain) - ioContext.Header.FirstMiniFatSectorId = newSectorIndex; + Header header = ioContext.Header; + if (header.FirstMiniFatSectorId == SectorType.EndOfChain) + header.FirstMiniFatSectorId = newSectorIndex; + header.MiniFatSectorCount++; miniFatEnumerator.Reset(); // TODO: Jump closer to the new sector From 02c236c8ce7da88a4eea3bf6e3acba1a1216cd05 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 13:51:21 +1300 Subject: [PATCH 076/114] Set directory sector count for V4 --- OpenMcdf3/Directories.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/Directories.cs b/OpenMcdf3/Directories.cs index 186043ab..a90d57f6 100644 --- a/OpenMcdf3/Directories.cs +++ b/OpenMcdf3/Directories.cs @@ -55,8 +55,11 @@ public DirectoryEntry CreateOrRecycleDirectoryEntry() CfbBinaryWriter writer = ioContext.Writer; uint id = fatChainEnumerator.Extend(); - if (ioContext.Header.FirstDirectorySectorId == SectorType.EndOfChain) - ioContext.Header.FirstDirectorySectorId = id; + Header header = ioContext.Header; + if (header.FirstDirectorySectorId == SectorType.EndOfChain) + header.FirstDirectorySectorId = id; + if (ioContext.Version == Version.V4) + header.DirectorySectorCount++; Sector sector = new(id, ioContext.SectorSize); writer.Position = sector.Position; From cb9cb9d7cb0d5fa8e12577631f106dd810fb1966 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 10:19:24 +1300 Subject: [PATCH 077/114] Use byte[] for directory entry name --- OpenMcdf3.Tests/BinaryWriterTests.cs | 8 +++- OpenMcdf3/CfbBinaryReader.cs | 9 ++--- OpenMcdf3/CfbBinaryWriter.cs | 6 +-- OpenMcdf3/DirectoryEntry.cs | 56 +++++++++++++++++++--------- OpenMcdf3/DirectoryEntryComparer.cs | 46 +++++++++++++++++++++++ OpenMcdf3/DirectoryTreeEnumerator.cs | 4 +- 6 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 OpenMcdf3/DirectoryEntryComparer.cs diff --git a/OpenMcdf3.Tests/BinaryWriterTests.cs b/OpenMcdf3.Tests/BinaryWriterTests.cs index 6e5f4722..cd45cff0 100644 --- a/OpenMcdf3.Tests/BinaryWriterTests.cs +++ b/OpenMcdf3.Tests/BinaryWriterTests.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf3.Tests; +using System.Text; + +namespace OpenMcdf3.Tests; [TestClass] public sealed class BinaryWriterTests @@ -44,7 +46,6 @@ public void WriteDirectoryEntry() { DirectoryEntry expected = new() { - Name = "Root Entry", Type = StorageType.Storage, Color = NodeColor.Red, LeftSiblingId = 2, @@ -58,6 +59,9 @@ public void WriteDirectoryEntry() StreamLength = 7 }; + string name = "Root Entry"; + expected.NameLength = (ushort)Encoding.Unicode.GetBytes(name, 0, name.Length, expected.Name, 0); + using MemoryStream stream = new(); using CfbBinaryWriter writer = new(stream); writer.Write(expected); diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index c9c6b0f6..51cbf2ae 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -123,15 +123,12 @@ public DirectoryEntry ReadDirectoryEntry(Version version, uint sid) if (version is not Version.V3 and not Version.V4) throw new ArgumentException($"Unsupported version: {version}.", nameof(version)); - Read(buffer, 0, DirectoryEntry.NameFieldLength); // TODO - ushort nameLength = ReadUInt16(); - int clampedNameLength = Math.Max(0, Math.Min(DirectoryEntry.NameFieldLength, nameLength - 2)); - string name = Encoding.Unicode.GetString(buffer, 0, clampedNameLength); + Read(buffer, 0, DirectoryEntry.NameFieldLength); DirectoryEntry entry = new() { Id = sid, - Name = name, + NameLength = ReadUInt16(), Type = ReadStorageType(), Color = ReadColor(), LeftSiblingId = ReadUInt32(), @@ -144,6 +141,8 @@ public DirectoryEntry ReadDirectoryEntry(Version version, uint sid) StartSectorId = ReadUInt32() }; + Buffer.BlockCopy(buffer, 0, entry.Name, 0, DirectoryEntry.NameFieldLength); + if (version == Version.V3) { entry.StreamLength = ReadUInt32(); diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index 73e65954..5ff8c4a6 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -69,10 +69,8 @@ public void Write(Header header) public void Write(DirectoryEntry entry) { - buffer.AsSpan().Clear(); - int nameLength = Encoding.Unicode.GetBytes(entry.Name, 0, entry.Name.Length, buffer, 0); - Write(buffer, 0, DirectoryEntry.NameFieldLength); - Write((short)(nameLength + 2)); + Write(entry.Name, 0, DirectoryEntry.NameFieldLength); + Write(entry.NameLength); Write((byte)entry.Type); Write((byte)entry.Color); Write(entry.LeftSiblingId); diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index f08cf2ce..081fc99c 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -1,4 +1,7 @@ -namespace OpenMcdf3; +using System.Runtime.InteropServices; +using System.Text; + +namespace OpenMcdf3; /// /// The storage type of a . @@ -42,21 +45,14 @@ internal sealed class DirectoryEntry : IEquatable internal static readonly byte[] Unallocated = new byte[128]; - string name = string.Empty; DateTime creationTime; DateTime modifiedTime; public uint Id { get; set; } - public string Name - { - get => name; - set - { - ThrowHelper.ThrowIfNameIsInvalid(value); - name = value; - } - } + public byte[] Name { get; } = new byte[NameFieldLength]; + + public ushort NameLength { get; set; } /// /// The type of the storage object. @@ -137,12 +133,34 @@ public DateTime ModifiedTime _ => '?' }; + public ReadOnlySpan NameByteSpan + { + get + { + int clampedNameLength = Math.Max(0, Math.Min(NameFieldLength, NameLength - 2)); + return Name.AsSpan(0, clampedNameLength); + } + } + + public ReadOnlySpan NameCharSpan => MemoryMarshal.Cast(NameByteSpan); + + public string NameString + { + get + { + int clampedNameLength = Math.Max(0, Math.Min(NameFieldLength, NameLength - 2)); + return Encoding.Unicode.GetString(Name, 0, clampedNameLength); + } + set => NameLength = (ushort)(Encoding.Unicode.GetBytes(value, 0, value.Length, Name, 0) + 2); + } + public override bool Equals(object? obj) => Equals(obj as DirectoryEntry); public bool Equals(DirectoryEntry? other) { return other is not null - && Name == other.Name + && Name.SequenceEqual(other.Name) + && NameLength == other.NameLength && Type == other.Type && Color == other.Color && LeftSiblingId == other.LeftSiblingId @@ -163,8 +181,8 @@ public bool Equals(DirectoryEntry? other) public void Recycle(StorageType storageType, string name) { Type = storageType; + NameString = name; Color = NodeColor.Black; - Name = name; LeftSiblingId = StreamId.NoStream; RightSiblingId = StreamId.NoStream; ChildId = StreamId.NoStream; @@ -189,16 +207,16 @@ public void Recycle(StorageType storageType, string name) } } - public EntryInfo ToEntryInfo() => new(Name, StreamLength); + public EntryInfo ToEntryInfo() => new(NameString, StreamLength); - public override string ToString() => $"{Id}: \"{Name}\""; + public override string ToString() => $"{Id}: \"{NameString}\""; public DirectoryEntry Clone() { - return new DirectoryEntry + DirectoryEntry clone = new() { Id = Id, - Name = Name, + NameLength = NameLength, Type = Type, Color = Color, LeftSiblingId = LeftSiblingId, @@ -211,5 +229,9 @@ public DirectoryEntry Clone() StartSectorId = StreamId.NoStream, StreamLength = 0 }; + + Array.Copy(Name, clone.Name, Name.Length); + + return clone; } } diff --git a/OpenMcdf3/DirectoryEntryComparer.cs b/OpenMcdf3/DirectoryEntryComparer.cs new file mode 100644 index 00000000..f4799b15 --- /dev/null +++ b/OpenMcdf3/DirectoryEntryComparer.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; + +namespace OpenMcdf3; + +internal class DirectoryEntryComparer : IComparer +{ + public static DirectoryEntryComparer Default { get; } = new(); + + public static int Compare(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length < y.Length) + return -1; + + if (x.Length > y.Length) + return 1; + + for (int i = 0; i < x.Length; i++) + { + char xChar = char.ToUpperInvariant(x[i]); + char yChar = char.ToUpperInvariant(y[i]); + + if (xChar < yChar) + return -1; + if (xChar > yChar) + return 1; + } + + return 0; + } + + public int Compare(DirectoryEntry? x, DirectoryEntry? y) + { + Debug.Assert(x is not null && y is not null); + + if (x == null && y == null) + return 0; + + if (x is null) + return -1; + + if (y is null) + return 1; + + return Compare(x.NameCharSpan, y.NameCharSpan); + } +} diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 8c840901..0fec3c74 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -87,9 +87,11 @@ public bool MoveTo(string name) { Reset(); + ReadOnlySpan nameSpan = name.AsSpan(); + while (MoveNext()) { - if (Current.Name == name) + if (DirectoryEntryComparer.Compare(Current.NameCharSpan, nameSpan) == 0) return true; } From 92cc9d74b7c4a63cf78625fbeaa5fa9207a01344 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Thu, 7 Nov 2024 22:18:16 +1300 Subject: [PATCH 078/114] Add initial DirectoryTree implementation --- OpenMcdf3.Tests/StorageTests.cs | 4 + OpenMcdf3/Directories.cs | 6 + OpenMcdf3/DirectoryTree.cs | 229 +++++++++++++++++++++++++++ OpenMcdf3/DirectoryTreeEnumerator.cs | 129 ++------------- OpenMcdf3/Storage.cs | 26 ++- 5 files changed, 259 insertions(+), 135 deletions(-) create mode 100644 OpenMcdf3/DirectoryTree.cs diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 003a4b87..62f16f77 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -31,6 +31,7 @@ public void CreateStorage(Version version, int subStorageCount) { for (int i = 0; i < subStorageCount; i++) rootStorage.CreateStorage($"Test{i}"); + rootStorage.TraceDirectoryEntries(DebugWriter.Default); } memoryStream.Position = 0; @@ -38,6 +39,9 @@ public void CreateStorage(Version version, int subStorageCount) { IEnumerable entries = rootStorage.EnumerateEntries(); Assert.AreEqual(subStorageCount, entries.Count()); + + for (int i = 0; i < subStorageCount; i++) + rootStorage.OpenStorage($"Test{i}"); } } diff --git a/OpenMcdf3/Directories.cs b/OpenMcdf3/Directories.cs index a90d57f6..f96513d9 100644 --- a/OpenMcdf3/Directories.cs +++ b/OpenMcdf3/Directories.cs @@ -32,6 +32,12 @@ public DirectoryEntry GetDictionaryEntry(uint streamId) public bool TryGetDictionaryEntry(uint streamId, out DirectoryEntry? entry) { + if (streamId == StreamId.NoStream) + { + entry = null; + return false; + } + if (streamId > StreamId.Maximum) throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}.", nameof(streamId)); diff --git a/OpenMcdf3/DirectoryTree.cs b/OpenMcdf3/DirectoryTree.cs new file mode 100644 index 00000000..c8077b9a --- /dev/null +++ b/OpenMcdf3/DirectoryTree.cs @@ -0,0 +1,229 @@ +using System.Diagnostics; + +namespace OpenMcdf3; + +internal class DirectoryTree +{ + internal enum RelationType + { + Previous, + Next, + Directory, + } + + private readonly Directories directories; + private readonly DirectoryEntry root; + + public DirectoryTree(Directories directories, DirectoryEntry root) + { + this.directories = directories; + this.root = root; + } + + public bool TryGetDirectoryEntry(string name, out DirectoryEntry? entry) + { + if (!directories.TryGetDictionaryEntry(root.ChildId, out DirectoryEntry? child)) + { + entry = null; + return false; + } + + ReadOnlySpan nameSpan = name.AsSpan(); + while (child is not null) + { + int compare = DirectoryEntryComparer.Compare(nameSpan, child.NameCharSpan); + if (compare < 0) + { + directories.TryGetDictionaryEntry(child.LeftSiblingId, out child); + } + else if (compare > 0) + { + directories.TryGetDictionaryEntry(child.RightSiblingId, out child); + } + else + { + entry = child; + return true; + } + } + + entry = null; + return false; + } + + public DirectoryEntry GetParent(DirectoryEntry entry, out RelationType relation) + { + if (!TryGetParent(entry, out DirectoryEntry? parent, out relation)) + throw new KeyNotFoundException($"DirectoryEntry {entry} has no parent."); + return parent!; + } + + public bool TryGetParent(DirectoryEntry entry, out DirectoryEntry? parent, out RelationType relation) + { + if (!directories.TryGetDictionaryEntry(root.ChildId, out DirectoryEntry? child)) + { + parent = null; + relation = RelationType.Directory; + return false; + } + + parent = root; + relation = RelationType.Directory; + while (child is not null) + { + int compare = DirectoryEntryComparer.Compare(entry.NameCharSpan, child.NameCharSpan); + if (compare < 0) + { + parent = child; + relation = RelationType.Previous; + directories.TryGetDictionaryEntry(child.LeftSiblingId, out child); + } + else if (compare > 0) + { + parent = child; + relation = RelationType.Next; + directories.TryGetDictionaryEntry(child.RightSiblingId, out child); + } + else + { + return true; + } + } + + return false; + } + + public void Add(DirectoryEntry entry) + { + if (!directories.TryGetDictionaryEntry(root.ChildId, out DirectoryEntry? currentEntry)) + { + root.ChildId = entry.Id; + directories.Write(root); + directories.Write(entry); + return; + } + + uint previous = currentEntry!.LeftSiblingId; + uint next = currentEntry.RightSiblingId; + + while (true) + { + int compare = DirectoryEntryComparer.Compare(entry.NameCharSpan, currentEntry!.NameCharSpan); + if (compare < 0) + { + if (previous == StreamId.NoStream) + { + currentEntry.LeftSiblingId = entry.Id; + directories.Write(currentEntry); + directories.Write(entry); + return; + } + + currentEntry = directories.GetDictionaryEntry(previous); + } + else if (compare > 0) + { + if (next == StreamId.NoStream) + { + currentEntry.RightSiblingId = entry.Id; + directories.Write(currentEntry); + directories.Write(entry); + return; + } + + currentEntry = directories.GetDictionaryEntry(next); + } + else + { + throw new IOException($"{entry.Type} \"{entry.NameString}\" already exists."); + } + + previous = currentEntry!.LeftSiblingId; + next = currentEntry!.RightSiblingId; + } + } + + void SetRelation(DirectoryEntry entry, RelationType relation, uint value) + { + switch (relation) + { + case RelationType.Previous: + entry.LeftSiblingId = value; + break; + case RelationType.Next: + entry.RightSiblingId = value; + break; + case RelationType.Directory: + root.ChildId = value; + break; + } + } + + public void Remove(DirectoryEntry entry) + { + DirectoryEntry parent = GetParent(entry, out RelationType relation); + + if (entry.LeftSiblingId == StreamId.NoStream) + { + SetRelation(parent, relation, entry.RightSiblingId); + directories.Write(parent); + } + else + { + SetRelation(parent, relation, entry.LeftSiblingId); + directories.Write(parent); + + if (entry.RightSiblingId != StreamId.NoStream) + { + uint newRightChildParent = entry.LeftSiblingId; + DirectoryEntry newRightChildParentEntry; + for (; ; ) + { + newRightChildParentEntry = directories.GetDictionaryEntry(newRightChildParent); + if (newRightChildParentEntry.RightSiblingId != StreamId.NoStream) + { + break; + } + }; + + newRightChildParentEntry.RightSiblingId = entry.RightSiblingId; + directories.Write(newRightChildParentEntry); + } + } + } + + public void WriteTrace(TextWriter writer) + { + if (root is null) + { + Trace.WriteLine(""); + return; + } + + Stack<(DirectoryEntry node, int indentLevel)> stack = new Stack<(DirectoryEntry, int)>(); + directories.TryGetDictionaryEntry(root.ChildId, out DirectoryEntry? current); + int currentIndentLevel = 0; + + while (stack.Count > 0 || current is not null) + { + if (current != null) + { + stack.Push((current, currentIndentLevel)); + directories.TryGetDictionaryEntry(current.RightSiblingId, out current); + currentIndentLevel++; + } + else + { + (DirectoryEntry node, int indentLevel) = stack.Pop(); + currentIndentLevel = indentLevel; + + for (int i = 0; i < indentLevel; i++) + writer.Write(" "); + writer.WriteLine(node.Color == NodeColor.Black ? $" {node} " : $"<{node}>"); + + directories.TryGetDictionaryEntry(node.LeftSiblingId, out current); + currentIndentLevel++; + } + } + } +} diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 0fec3c74..91b93b9d 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -10,19 +10,14 @@ internal sealed class DirectoryTreeEnumerator : IEnumerator { private readonly Directories directories; private readonly DirectoryEntry root; - private DirectoryEntry? child; private readonly Stack stack = new(); - DirectoryEntry parent; DirectoryEntry? current; internal DirectoryTreeEnumerator(Directories directories, DirectoryEntry root) { this.directories = directories; this.root = root; - if (root.ChildId != StreamId.NoStream) - child = directories.GetDictionaryEntry(root.ChildId); - parent = root; - PushLeft(child); + Reset(); } /// @@ -50,17 +45,12 @@ public bool MoveNext() if (stack.Count == 0) { current = null; - parent = root; return false; } current = stack.Pop(); - parent = stack.Count == 0 ? root : stack.Peek(); - if (current.RightSiblingId != StreamId.NoStream) - { - DirectoryEntry rightSibling = directories.GetDictionaryEntry(current.RightSiblingId); - PushLeft(rightSibling); - } + if (directories.TryGetDictionaryEntry(current.RightSiblingId, out DirectoryEntry? rightSibling)) + PushLeft(rightSibling!); return true; } @@ -69,9 +59,12 @@ public bool MoveNext() public void Reset() { current = null; - parent = root; stack.Clear(); - PushLeft(child); + if (root.ChildId != StreamId.NoStream) + { + DirectoryEntry child = directories.GetDictionaryEntry(root.ChildId); + PushLeft(child); + } } private void PushLeft(DirectoryEntry? node) @@ -79,111 +72,7 @@ private void PushLeft(DirectoryEntry? node) while (node is not null) { stack.Push(node); - node = node.LeftSiblingId == StreamId.NoStream ? null : directories.GetDictionaryEntry(node.LeftSiblingId); - } - } - - public bool MoveTo(string name) - { - Reset(); - - ReadOnlySpan nameSpan = name.AsSpan(); - - while (MoveNext()) - { - if (DirectoryEntryComparer.Compare(Current.NameCharSpan, nameSpan) == 0) - return true; - } - - return false; - } - - public DirectoryEntry? TryGetDirectoryEntry(string name) - { - if (MoveTo(name)) - return Current; - return null; - } - - public DirectoryEntry Add(StorageType storageType, string name) - { - if (MoveTo(name)) - throw new IOException($"{storageType} \"{name}\" already exists."); - - DirectoryEntry entry = directories.CreateOrRecycleDirectoryEntry(); - entry.Recycle(storageType, name); - - Add(entry); - - return entry; - } - - void Add(DirectoryEntry entry) - { - Reset(); - - // TODO: Implement balancing (all-black for now) - entry.Color = NodeColor.Black; - directories.Write(entry); - - if (root.ChildId == StreamId.NoStream) - { - Debug.Assert(child is null); - root.ChildId = entry.Id; - directories.Write(root); - child = entry; - } - else - { - Debug.Assert(child is not null); - DirectoryEntry node = child!; - while (node.LeftSiblingId != StreamId.NoStream) - node = directories.GetDictionaryEntry(node.LeftSiblingId); - node.LeftSiblingId = entry.Id; - directories.Write(node); - } - } - - public void Remove(DirectoryEntry entry) - { - if (child is null) - throw new KeyNotFoundException("DirectoryEntry has no children"); - - if (root.ChildId == entry.Id) - { - root.ChildId = entry.LeftSiblingId; - directories.Write(root); - if (root.ChildId == StreamId.NoStream) - child = null; - return; - } - - Reset(); - - while (MoveNext()) - { - if (current!.Id == entry.Id) - { - if (parent.LeftSiblingId == entry.Id) - parent.LeftSiblingId = entry.LeftSiblingId; - directories.Write(parent); - - entry.Recycle(); - directories.Write(entry); - break; - } - } - } - - internal void PrintTrace(TextWriter writer) - { - Reset(); - - while (MoveNext()) - { - for (int i = 0; i < stack.Count; i++) - writer.Write(" "); - writer.WriteLine($"{Current.ColorChar} {Current}"); + directories.TryGetDictionaryEntry(node.LeftSiblingId, out node); } } } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 4182f093..33cf3b76 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -6,6 +6,7 @@ public class Storage { internal readonly IOContext ioContext; + internal readonly DirectoryTree directoryTree; internal DirectoryEntry DirectoryEntry { get; } @@ -15,6 +16,7 @@ internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) throw new ArgumentException("DirectoryEntry must be a Storage or Root.", nameof(directoryEntry)); this.ioContext = ioContext; + directoryTree = new(ioContext.Directories, directoryEntry); DirectoryEntry = directoryEntry; } @@ -46,16 +48,12 @@ IEnumerable EnumerateDirectoryEntries() IEnumerable EnumerateDirectoryEntries(StorageType type) => EnumerateDirectoryEntries() .Where(e => e.Type == type); - DirectoryEntry? TryGetDirectoryEntry(string name) - { - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); - return directoryTreeEnumerator.TryGetDirectoryEntry(name); - } - DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) { - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); - return directoryTreeEnumerator.Add(storageType, name); + DirectoryEntry entry = ioContext.Directories.CreateOrRecycleDirectoryEntry(); + entry.Recycle(storageType, name); + directoryTree.Add(entry); + return entry; } public Storage CreateStorage(string name) @@ -85,7 +83,7 @@ public Storage OpenStorage(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - DirectoryEntry? entry = TryGetDirectoryEntry(name); + directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null || entry.Type is not StorageType.Storage) throw new DirectoryNotFoundException($"Storage not found: {name}."); return new Storage(ioContext, entry); @@ -97,7 +95,7 @@ public CfbStream OpenStream(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - DirectoryEntry? entry = TryGetDirectoryEntry(name); + directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null || entry.Type is not StorageType.Stream) throw new FileNotFoundException($"Stream not found: {name}.", name); @@ -111,8 +109,7 @@ public void Delete(string name) this.ThrowIfDisposed(ioContext.IsDisposed); - using DirectoryTreeEnumerator directoryTreeEnumerator = new(ioContext.Directories, DirectoryEntry); - DirectoryEntry? entry = directoryTreeEnumerator.TryGetDirectoryEntry(name); + directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null) return; @@ -139,12 +136,11 @@ public void Delete(string name) } } - directoryTreeEnumerator.Remove(entry); + directoryTree.Remove(entry); } internal void TraceDirectoryEntries(TextWriter writer) { - using DirectoryTreeEnumerator treeEnumerator = new(ioContext.Directories, DirectoryEntry); - treeEnumerator.PrintTrace(writer); + directoryTree.WriteTrace(writer); } } From 59e907ffc51df34228cbff60689d381afd38319d Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 11:56:39 +1300 Subject: [PATCH 079/114] Add structured storage reference tests --- OpenMcdf3.Tests/OpenMcdf3.Tests.csproj | 6 +- OpenMcdf3.Tests/StorageTests.cs | 55 ++- OpenMcdf3.Tests/StreamTests.cs | 16 +- OpenMcdf3.sln | 8 +- OpenMcdf3/RootStorage.cs | 2 +- StructuredStorage/LockBytes.cs | 50 +++ StructuredStorage/NativeMethods.json | 6 + StructuredStorage/NativeMethods.txt | 38 +++ StructuredStorage/PropertySetStorage.cs | 50 +++ StructuredStorage/PropertyStorage.cs | 167 ++++++++++ StructuredStorage/Storage.cs | 370 +++++++++++++++++++++ StructuredStorage/Stream.cs | 185 +++++++++++ StructuredStorage/StructuredStorage.csproj | 18 + 13 files changed, 944 insertions(+), 27 deletions(-) create mode 100644 StructuredStorage/LockBytes.cs create mode 100644 StructuredStorage/NativeMethods.json create mode 100644 StructuredStorage/NativeMethods.txt create mode 100644 StructuredStorage/PropertySetStorage.cs create mode 100644 StructuredStorage/PropertyStorage.cs create mode 100644 StructuredStorage/Storage.cs create mode 100644 StructuredStorage/Stream.cs create mode 100644 StructuredStorage/StructuredStorage.csproj diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj index 1a72e048..7b0ea833 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj @@ -1,7 +1,7 @@ - net48;net8.0 + net48;net8.0-windows Exe 11.0 enable @@ -17,6 +17,10 @@ + + + + diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 62f16f77..2390ee51 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -10,9 +10,19 @@ public sealed class StorageTests [DataRow("MultipleStorage4.cfs", 1)] public void Read(string fileName, long storageCount) { - using var rootStorage = RootStorage.OpenRead(fileName); - IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); - Assert.AreEqual(storageCount, storageEntries.Count()); + using (var rootStorage = RootStorage.OpenRead(fileName, StorageModeFlags.LeaveOpen)) + { + IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); + Assert.AreEqual(storageCount, storageEntries.Count()); + } + +#if WINDOWS + using (var rootStorage = StructuredStorage.Storage.Open(fileName)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + Assert.AreEqual(storageCount, entries.Count()); + } +#endif } [TestMethod] @@ -27,7 +37,7 @@ public void Read(string fileName, long storageCount) public void CreateStorage(Version version, int subStorageCount) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { for (int i = 0; i < subStorageCount; i++) rootStorage.CreateStorage($"Test{i}"); @@ -35,7 +45,7 @@ public void CreateStorage(Version version, int subStorageCount) } memoryStream.Position = 0; - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { IEnumerable entries = rootStorage.EnumerateEntries(); Assert.AreEqual(subStorageCount, entries.Count()); @@ -43,6 +53,19 @@ public void CreateStorage(Version version, int subStorageCount) for (int i = 0; i < subStorageCount; i++) rootStorage.OpenStorage($"Test{i}"); } + +#if WINDOWS + using (var rootStorage = StructuredStorage.Storage.Open(memoryStream)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + Assert.AreEqual(subStorageCount, entries.Count()); + + for (int i = 0; i < subStorageCount; i++) + { + using StructuredStorage.Storage storage = rootStorage.OpenStorage($"Test{i}"); + } + } +#endif } [TestMethod] @@ -62,13 +85,13 @@ public void CreateDuplicateStorageThrowsException(Version version) public void DeleteSingleStorage(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { rootStorage.CreateStorage("Test"); Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { rootStorage.Delete("Test"); Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); @@ -86,14 +109,14 @@ public void DeleteSingleStorage(Version version) public void DeleteRedBlackTreeChildLeaf(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { rootStorage.CreateStorage("Test1"); rootStorage.CreateStorage("Test2"); Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { rootStorage.Delete("Test1"); Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); @@ -111,14 +134,14 @@ public void DeleteRedBlackTreeChildLeaf(Version version) public void DeleteRedBlackTreeSiblingLeaf(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { rootStorage.CreateStorage("Test1"); rootStorage.CreateStorage("Test2"); Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { rootStorage.Delete("Test2"); Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); @@ -136,7 +159,7 @@ public void DeleteRedBlackTreeSiblingLeaf(Version version) public void DeleteRedBlackTreeSibling(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { rootStorage.CreateStorage("Test1"); rootStorage.CreateStorage("Test2"); @@ -144,7 +167,7 @@ public void DeleteRedBlackTreeSibling(Version version) Assert.AreEqual(3, rootStorage.EnumerateEntries().Count()); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { rootStorage.Delete("Test2"); Assert.AreEqual(2, rootStorage.EnumerateEntries().Count()); @@ -162,7 +185,7 @@ public void DeleteRedBlackTreeSibling(Version version) public void DeleteStorageRecursively(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { Storage storage = rootStorage.CreateStorage("Test"); Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); @@ -170,7 +193,7 @@ public void DeleteStorageRecursively(Version version) using CfbStream stream = storage.CreateStream("Test"); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { rootStorage.Delete("Test"); Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); @@ -188,7 +211,7 @@ public void DeleteStorageRecursively(Version version) public void DeleteStream(Version version) { using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { rootStorage.CreateStream("Test"); Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 717444a5..7a0f030f 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -179,7 +179,7 @@ public void WriteThenRead(Version version, int length) expectedBuffer[i] = (byte)i; using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream"); Assert.AreEqual(0, stream.Length); @@ -288,7 +288,7 @@ public void Modify(Version version, int length) expectedBuffer[i] = (byte)i; using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream1"); Assert.AreEqual(0, stream.Length); @@ -296,7 +296,7 @@ public void Modify(Version version, int length) stream.Write(expectedBuffer, 0, expectedBuffer.Length); } - using (var rootStorage = RootStorage.Open(memoryStream)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream2"); Assert.AreEqual(0, stream.Length); @@ -364,7 +364,7 @@ public void ModifyCommit(Version version, int length) expectedBuffer[i] = (byte)i; using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream1"); Assert.AreEqual(0, stream.Length); @@ -372,7 +372,7 @@ public void ModifyCommit(Version version, int length) stream.Write(expectedBuffer, 0, expectedBuffer.Length); } - using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.Transacted)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted)) { using CfbStream stream = rootStorage.CreateStream("TestStream2"); Assert.AreEqual(0, stream.Length); @@ -442,7 +442,7 @@ public void TransactedRead(Version version, int length) expectedBuffer[i] = (byte)i; using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream1"); Assert.AreEqual(0, stream.Length); @@ -495,7 +495,7 @@ public void ModifyRevert(Version version, int length) expectedBuffer[i] = (byte)i; using MemoryStream memoryStream = new(); - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) { using CfbStream stream = rootStorage.CreateStream("TestStream1"); Assert.AreEqual(0, stream.Length); @@ -503,7 +503,7 @@ public void ModifyRevert(Version version, int length) stream.Write(expectedBuffer, 0, expectedBuffer.Length); } - using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.Transacted)) + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted)) { using CfbStream stream = rootStorage.CreateStream("TestStream2"); Assert.AreEqual(0, stream.Length); diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 6d425491..7f045797 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -15,7 +15,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Benchmarks", "OpenMcdf3.Benchmarks\OpenMcdf3.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf3.Perf", "OpenMcdf3.Perf\OpenMcdf3.Perf.csproj", "{8167F453-A244-4FE2-9B33-A7B80B1B7AB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Perf", "OpenMcdf3.Perf\OpenMcdf3.Perf.csproj", "{8167F453-A244-4FE2-9B33-A7B80B1B7AB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorage", "StructuredStorage\StructuredStorage.csproj", "{D7861D73-B42C-403E-9B9E-F921BC70F0D3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Debug|Any CPU.Build.0 = Debug|Any CPU {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Release|Any CPU.ActiveCfg = Release|Any CPU {8167F453-A244-4FE2-9B33-A7B80B1B7AB1}.Release|Any CPU.Build.0 = Release|Any CPU + {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index fcdd90c2..3d452d30 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -48,7 +48,7 @@ public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags return Open(stream); } - public static RootStorage OpenRead(string fileName) + public static RootStorage OpenRead(string fileName, StorageModeFlags flags = StorageModeFlags.None) { FileStream stream = File.OpenRead(fileName); return Open(stream); diff --git a/StructuredStorage/LockBytes.cs b/StructuredStorage/LockBytes.cs new file mode 100644 index 00000000..a9688958 --- /dev/null +++ b/StructuredStorage/LockBytes.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com.StructuredStorage; + +namespace StructuredStorage; + +/// +/// Encapsulates ILockBytes over an HGlobal allocation. +/// +internal sealed class LockBytes : IDisposable +{ + readonly ILockBytes lockBytes; + private bool disposedValue; + + public LockBytes(int count) + { + IntPtr hGlobal = Marshal.AllocHGlobal(count); + HRESULT hr = PInvoke.CreateILockBytesOnHGlobal((HGLOBAL)hGlobal, true, out lockBytes); + hr.ThrowOnFailure(); + } + + public LockBytes(MemoryStream stream) + { + var hGlobal = (HGLOBAL)Marshal.AllocHGlobal((int)stream.Length); + Marshal.Copy(stream.GetBuffer(), 0, hGlobal, (int)stream.Length); + HRESULT hr = PInvoke.CreateILockBytesOnHGlobal(hGlobal, true, out lockBytes); + hr.ThrowOnFailure(); + } + + public void Dispose() + { + if (disposedValue) + return; + + int count = Marshal.ReleaseComObject(lockBytes); + Debug.Assert(count == 0); + + disposedValue = true; + GC.SuppressFinalize(this); + } + + ~LockBytes() + { + Dispose(); + } + + internal ILockBytes ILockBytes => lockBytes; +} diff --git a/StructuredStorage/NativeMethods.json b/StructuredStorage/NativeMethods.json new file mode 100644 index 00000000..66108bf1 --- /dev/null +++ b/StructuredStorage/NativeMethods.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "wideCharOnly": true, + "emitSingleFile": true, + "public": false +} diff --git a/StructuredStorage/NativeMethods.txt b/StructuredStorage/NativeMethods.txt new file mode 100644 index 00000000..831d6ca4 --- /dev/null +++ b/StructuredStorage/NativeMethods.txt @@ -0,0 +1,38 @@ +// Structured storage + +// Functions +CreateILockBytesOnHGlobal +PropVariantToVariant +ReadClassStg +ReadClassStm +SHCreateItemFromParsingName +SHCreateStreamOnFile +StgCreateDocfileOnILockBytes +StgCreateStorageEx +StgIsStorageFile +StgOpenStorage +StgOpenStorageEx +StgOpenStorageOnILockBytes +VariantClear +VariantToPropVariant +WriteClassStg +WriteClassStm + +// Interfaces +IEnumSTATPROPSETSTG +IEnumSTATPROPSTG +IEnumSTATSTG +ILockBytes +IPropertySetStorage +IPropertyStorage +IRootStorage +IStorage +IStream + +// Enumerations +PROPSETFLAG_* +STGTY + +// Constants +STG_E_* +PROPSETFLAG_* diff --git a/StructuredStorage/PropertySetStorage.cs b/StructuredStorage/PropertySetStorage.cs new file mode 100644 index 00000000..7c6df922 --- /dev/null +++ b/StructuredStorage/PropertySetStorage.cs @@ -0,0 +1,50 @@ +using Windows.Win32; +using Windows.Win32.System.Com.StructuredStorage; + +namespace StructuredStorage; + +/// +/// Wraps IPropertySetStorage. +/// +public sealed class PropertySetStorage +{ + /// + /// PROPSETFLAG constants. + /// + [Flags] +#pragma warning disable CA1008 + public enum Flags + { + Default = (int)PInvoke.PROPSETFLAG_DEFAULT, + NonSimple = (int)PInvoke.PROPSETFLAG_NONSIMPLE, + ANSI = (int)PInvoke.PROPSETFLAG_ANSI, + Unbuffered = (int)PInvoke.PROPSETFLAG_UNBUFFERED, + CaseSensitive = (int)PInvoke.PROPSETFLAG_CASE_SENSITIVE, + } +#pragma warning restore CA1008 + + private readonly IPropertySetStorage propSet; // Cast of IStorage does not need disposal + + internal PropertySetStorage(IStorage storage) + { + propSet = (IPropertySetStorage)storage; + } + + public PropertyStorage Create(Guid formatID, StorageModes mode) => Create(formatID, Flags.Default, mode, Guid.Empty); + + public PropertyStorage Create(Guid formatID, Flags flags = Flags.Default, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) => Create(formatID, flags, mode, Guid.Empty); + + public unsafe PropertyStorage Create(Guid formatID, Flags flags, StorageModes mode, Guid classID) + { + propSet.Create(&formatID, &classID, (uint)flags, (uint)mode, out IPropertyStorage stg); + return new(stg); + } + + public unsafe PropertyStorage Open(Guid formatID, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) + { + propSet.Open(&formatID, (uint)mode, out IPropertyStorage propStorage); + return new(propStorage); + } + + public unsafe void Remove(Guid formatID) => propSet.Delete(&formatID); +} diff --git a/StructuredStorage/PropertyStorage.cs b/StructuredStorage/PropertyStorage.cs new file mode 100644 index 00000000..d713aca0 --- /dev/null +++ b/StructuredStorage/PropertyStorage.cs @@ -0,0 +1,167 @@ +using System.Collections; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com.StructuredStorage; + +namespace StructuredStorage; + +/// +/// Enumerates STATPROPSTG elements from a PropertyStorage. +/// +internal sealed class StatPropStgEnumerator : IEnumerator +{ + readonly IEnumSTATPROPSTG enumerator; + STATPROPSTG propStat; + + public STATPROPSTG Current => propStat; + + object IEnumerator.Current => propStat; + + public unsafe StatPropStgEnumerator(IPropertyStorage propertyStorage) + { + propertyStorage.Enum(out enumerator); + } + + public unsafe void Dispose() + { + FreeName(); + + Marshal.ReleaseComObject(enumerator); + } + + private unsafe void FreeName() + { + Marshal.FreeCoTaskMem((nint)propStat.lpwstrName.Value); + propStat.lpwstrName = null; + } + + public unsafe bool MoveNext() + { + FreeName(); + + fixed (STATPROPSTG* statPtr = &propStat) + { + uint fetched; + enumerator.Next(1, statPtr, &fetched); + return fetched > 0; + } + } + + public void Reset() + { + FreeName(); + + enumerator.Reset(); + } +} + +/// +/// Creates an enumerator for STATPROPSTG elements from a PropertyStorage. +/// +internal sealed class StatPropStgCollection : IEnumerable +{ + readonly IPropertyStorage propertyStorage; + + public StatPropStgCollection(IPropertyStorage propertyStorage) + { + this.propertyStorage = propertyStorage; + } + + public IEnumerator GetEnumerator() => new StatPropStgEnumerator(propertyStorage); + + IEnumerator IEnumerable.GetEnumerator() => new StatPropStgEnumerator(propertyStorage); +} + +/// +/// Wraps IPropertyStorage. +/// +public sealed class PropertyStorage : IDisposable +{ + private readonly IPropertyStorage propertyStorage; + private bool disposed; + + internal unsafe PropertyStorage(IPropertyStorage propertyStorage) + { + this.propertyStorage = propertyStorage; + StatPropStgCollection = new(propertyStorage); + + STATPROPSETSTG prop; + this.propertyStorage.Stat(&prop); + } + + #region IDisposable Members + + public void Dispose() + { + if (disposed) + return; + + int count = Marshal.ReleaseComObject(propertyStorage); + Debug.Assert(count == 0); + + disposed = true; + } + + #endregion + + internal StatPropStgCollection StatPropStgCollection { get; } + + public void Flush(CommitFlags flags = CommitFlags.Default) => propertyStorage.Commit((uint)flags); + + public unsafe void Remove(int propertyID) + { + PROPSPEC propspec = new() + { + ulKind = PROPSPEC_KIND.PRSPEC_PROPID, + Anonymous = new PROPSPEC._Anonymous_e__Union() + { + propid = (uint)propertyID, + }, + }; + propertyStorage.DeleteMultiple(1, &propspec); + } + + public void Revert() => propertyStorage.Revert(); + + public unsafe object? this[int propertyID] + { + get + { + PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID); + + var variants = new PROPVARIANT[1]; + propertyStorage.ReadMultiple(1, &spec, variants); + HRESULT hr = PInvoke.PropVariantToVariant(variants[0], out object variant); + hr.ThrowOnFailure(); + return variant; + } + + set + { + PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID); + + HRESULT hr = PInvoke.VariantToPropVariant(value, out PROPVARIANT pv); + hr.ThrowOnFailure(); + + PROPVARIANT[] pvs = [pv]; + propertyStorage.WriteMultiple(1, &spec, pvs, 2); + } + } +} + +static class PropVariantExtensions +{ + public static PROPSPEC CreatePropSpec(PROPSPEC_KIND kind, int propertyID) + { + return new PROPSPEC + { + ulKind = kind, + Anonymous = new PROPSPEC._Anonymous_e__Union + { + propid = (uint)propertyID, + }, + }; + } +} diff --git a/StructuredStorage/Storage.cs b/StructuredStorage/Storage.cs new file mode 100644 index 00000000..369df70b --- /dev/null +++ b/StructuredStorage/Storage.cs @@ -0,0 +1,370 @@ +using System.Collections; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; +using Windows.Win32.System.Com; +using Windows.Win32.System.Com.StructuredStorage; + +namespace StructuredStorage; + +#pragma warning disable CA1069 // Enums values should not be duplicated +#pragma warning disable CA1724 // Type names should not match namespaces +#pragma warning disable CA1028 // Enum storage should be Int32 +#pragma warning disable CA1008 // Enums should have zero value + +/// +/// STGC constants. +/// +[Flags] +public enum CommitFlags : uint +{ + Default = STGC.STGC_DEFAULT, + Overwrite = STGC.STGC_OVERWRITE, + OnlyIfCurrent = STGC.STGC_ONLYIFCURRENT, + DangerouslyCommitMerelyToDiskCache = STGC.STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE, + Consolidate = STGC.STGC_CONSOLIDATE, +} + +/// +/// STGM constants. +/// +[Flags] +public enum StorageModes : uint +{ + FailIfThere = STGM.STGM_FAILIFTHERE, + Direct = STGM.STGM_DIRECT, + AccessRead = STGM.STGM_READ, + AccessWrite = STGM.STGM_WRITE, + AccessReadWrite = STGM.STGM_READWRITE, + ShareExclusive = STGM.STGM_SHARE_EXCLUSIVE, + ShareDenyWrite = STGM.STGM_SHARE_DENY_WRITE, + ShareDenyRead = STGM.STGM_SHARE_DENY_READ, + ShareDenyNone = STGM.STGM_SHARE_DENY_NONE, + Create = STGM.STGM_CREATE, + Transacted = STGM.STGM_TRANSACTED, + Convert = STGM.STGM_CONVERT, + Priority = STGM.STGM_PRIORITY, + NoScratch = STGM.STGM_NOSCRATCH, + NoSnapShot = STGM.STGM_NOSNAPSHOT, + DirectSWMR = STGM.STGM_DIRECT_SWMR, + DeleteOnRelease = STGM.STGM_DELETEONRELEASE, + ModeSimple = STGM.STGM_SIMPLE, +} + +/// +/// Enumerates STATSTG elements from a Storage. +/// +internal sealed class StatStgEnumerator : IEnumerator +{ + readonly IEnumSTATSTG enumerator; + STATSTG stat; + + public STATSTG Current => stat; + + object IEnumerator.Current => stat; + + public unsafe StatStgEnumerator(IStorage storage) + { + storage.EnumElements(0, null, 0, out enumerator); + } + + public unsafe void Dispose() + { + FreeName(); + + Marshal.ReleaseComObject(enumerator); + } + + private unsafe void FreeName() + { + Marshal.FreeCoTaskMem((nint)stat.pwcsName.Value); + stat.pwcsName = null; + } + + public unsafe bool MoveNext() + { + FreeName(); + + fixed (STATSTG* statPtr = &stat) + { + uint fetched; + enumerator.Next(1, statPtr, &fetched); + return fetched > 0; + } + } + + public void Reset() + { + FreeName(); + + enumerator.Reset(); + } +} + +/// +/// Creates an enumerator for STATSTG elements from a Storage. +/// +internal sealed class StatStgCollection : IEnumerable +{ + readonly IStorage storage; + + public StatStgCollection(IStorage storage) + { + this.storage = storage; + } + + public IEnumerator GetEnumerator() => new StatStgEnumerator(storage); + + IEnumerator IEnumerable.GetEnumerator() => new StatStgEnumerator(storage); +} + +/// +/// Wraps a COM structured storage object. +/// +public sealed class Storage : IDisposable +{ + static readonly Guid IStorageGuid = typeof(IStorage).GUID; + + static STGOPTIONS V3Options => new() + { + usVersion = 2, + reserved = 0, + ulSectorSize = 512, + pwcsTemplateFile = null, + }; + + static STGOPTIONS V4Options => new() + { + usVersion = 2, + reserved = 0, + ulSectorSize = 4096, + pwcsTemplateFile = null, + }; + + readonly IStorage storage; + readonly LockBytes? lockBytes; // Prevents garbage collection of in-memory storage + + public Storage? Parent { get; } + + public PropertySetStorage PropertySetStorage { get; } + + internal StatStgCollection StatStgCollection { get; } + + bool disposed; + + // Methods + internal Storage(IStorage storage, Storage? parent = null, LockBytes? lockBytes = null) + { + this.storage = storage; + Parent = parent; + this.lockBytes = lockBytes; + PropertySetStorage = new(storage); + StatStgCollection = new StatStgCollection(storage); + } + + public static unsafe Storage Create(string fileName, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite, bool v4 = true) + { + STGOPTIONS opts = v4 ? V4Options : V3Options; + HRESULT hr = PInvoke.StgCreateStorageEx(fileName, (STGM)modes, STGFMT.STGFMT_DOCFILE, 0, &opts, (PSECURITY_DESCRIPTOR)null, IStorageGuid, out void* ptr); + hr.ThrowOnFailure(); + + var iStorage = (IStorage)Marshal.GetObjectForIUnknown((nint)ptr); + Marshal.Release((nint)ptr); + return new(iStorage); + } + + public static Storage CreateInMemory(int capacity) + { + LockBytes lockBytes = new(capacity); + HRESULT hr = PInvoke.StgCreateDocfileOnILockBytes(lockBytes.ILockBytes, STGM.STGM_READWRITE | STGM.STGM_SHARE_EXCLUSIVE | STGM.STGM_CREATE, 0, out IStorage storage); + hr.ThrowOnFailure(); + return new(storage, null, lockBytes); + } + + public static unsafe Storage Open(MemoryStream stream, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) + { + LockBytes lockBytes = new(stream); + HRESULT hr = PInvoke.StgOpenStorageOnILockBytes(lockBytes.ILockBytes, null, (STGM)modes, null, out IStorage storage); + hr.ThrowOnFailure(); + return new(storage); + } + + public static unsafe Storage Open(string fileName, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) + { + STGOPTIONS opts = V4Options; + HRESULT hr = PInvoke.StgOpenStorageEx(fileName, (STGM)modes, STGFMT.STGFMT_DOCFILE, 0, &opts, (PSECURITY_DESCRIPTOR)null, IStorageGuid, out void* ptr); + if (hr == HRESULT.STG_E_FILENOTFOUND) + throw new FileNotFoundException(null, fileName); + if (hr == HRESULT.STG_E_FILEALREADYEXISTS) + hr = HRESULT.STG_E_DOCFILECORRUPT; + hr.ThrowOnFailure(); + + var iStorage = (IStorage)Marshal.GetObjectForIUnknown((nint)ptr); + Marshal.Release((nint)ptr); + return new(iStorage); + } + + #region IDisposable Members + + public void Dispose() + { + if (disposed) + return; + + int count = Marshal.ReleaseComObject(storage); + Debug.Assert(count == 0); + + lockBytes?.Dispose(); + + disposed = true; + } + + #endregion + + public unsafe Storage CreateStorage(string name, StorageModes flags = StorageModes.Create | StorageModes.ShareExclusive | StorageModes.AccessReadWrite) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (char* namePtr = name) + { + storage.CreateStorage(namePtr, (STGM)flags, 0, 0, out IStorage childStorage); + return new Storage(childStorage, this); + } + } + + public unsafe Stream CreateStream(string name, StorageModes flags = StorageModes.Create | StorageModes.ShareExclusive | StorageModes.AccessReadWrite) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (char* namePtr = name) + { + storage.CreateStream(namePtr, (STGM)flags, 0, 0, out IStream stm); + return new Stream(stm, this); + } + } + + internal StatStgEnumerator CreateStatStgEnumerator() => new(storage); + + public IEnumerable EnumerateEntries() + { + ObjectDisposedException.ThrowIf(disposed, this); + + using StatStgEnumerator enumerator = CreateStatStgEnumerator(); + while (enumerator.MoveNext()) + { + yield return enumerator.Current.pwcsName.ToString(); + } + } + + public void DestroyElement(string name) + { + ObjectDisposedException.ThrowIf(disposed, this); + + storage.DestroyElement(name); + } + + public void DestroyElementIfExists(string name) + { + ObjectDisposedException.ThrowIf(disposed, this); + + if (ContainsElement(name)) + storage.DestroyElement(name); + } + + public bool ContainsElement(string name) + { + ObjectDisposedException.ThrowIf(disposed, this); + + return StatStgCollection.Any(s => s.pwcsName.AsSpan().SequenceEqual(name)); + } + + public bool ContainsStream(string name) + { + ObjectDisposedException.ThrowIf(disposed, this); + + return StatStgCollection.Any(s => (STGTY)s.type == STGTY.STGTY_STREAM && s.pwcsName.AsSpan().SequenceEqual(name)); + } + + public void Commit(CommitFlags flags = CommitFlags.Default) + { + ObjectDisposedException.ThrowIf(disposed, this); + + storage.Commit((uint)flags); + } + + public void MoveElement(string name, Storage destination) => MoveElement(name, destination, name); + + public void MoveElement(string name, Storage destination, string newName) + { + ObjectDisposedException.ThrowIf(disposed, this); + + storage.MoveElementTo(name, destination.storage, newName, 0); + } + + public unsafe Storage OpenStorage(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (char* namePtr = name) + { + storage.OpenStorage(namePtr, null, (STGM)flags, null, 0, out IStorage childStorage); + return new Storage(childStorage, this); + } + } + + public unsafe Stream OpenStream(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (char* namePtr = name) + { + storage.OpenStream(namePtr, null, (STGM)flags, 0, out IStream iStream); + return new Stream(iStream, this); + } + } + + public Stream OpenOrCreateStream(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive) + => ContainsStream(name) ? OpenStream(name, flags) : CreateStream(name, flags); + + public void Revert() + { + ObjectDisposedException.ThrowIf(disposed, this); + + storage.Revert(); + } + + public unsafe void SwitchToFile(string fileName) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (char* fileNamePtr = fileName) + { + if (storage is not IRootStorage rootStorage) + throw new InvalidOperationException("Not file storage"); + rootStorage.SwitchToFile(fileNamePtr); + } + } + + // Properties + public Guid Id + { + get + { + ObjectDisposedException.ThrowIf(disposed, this); + + HRESULT hr = PInvoke.ReadClassStg(storage, out Guid guid); + hr.ThrowOnFailure(); + return guid; + } + + set + { + ObjectDisposedException.ThrowIf(disposed, this); + + HRESULT hr = PInvoke.WriteClassStg(storage, value); + hr.ThrowOnFailure(); + } + } +} diff --git a/StructuredStorage/Stream.cs b/StructuredStorage/Stream.cs new file mode 100644 index 00000000..4b1df3f4 --- /dev/null +++ b/StructuredStorage/Stream.cs @@ -0,0 +1,185 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; + +namespace StructuredStorage; + +/// +/// Implements Stream on an COM IStream. +/// +public sealed class Stream : System.IO.Stream +{ + public Storage Parent { get; } + + readonly IStream stream; + bool disposed; + + internal Stream(IStream stream, Storage parent) + { + this.stream = stream; + Parent = parent; + + STGM mode = Stat.grfMode; + CanRead = mode.HasFlag(STGM.STGM_READWRITE) || !mode.HasFlag(STGM.STGM_WRITE); + CanWrite = mode.HasFlag(STGM.STGM_READWRITE) || mode.HasFlag(STGM.STGM_WRITE); + } + + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + Flush(); + + int count = Marshal.ReleaseComObject(stream); + Debug.Assert(count == 0); + } + + disposed = true; + + base.Dispose(disposing); + } + + public override void Flush() => Flush(CommitFlags.Default); + + public void Flush(CommitFlags flags) + { + ObjectDisposedException.ThrowIf(disposed, this); + + stream.Commit((STGC)flags); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(disposed, this); + + Span slice = buffer.AsSpan(offset, count); + return Read(slice); + } + + public override unsafe int Read(Span buffer) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (byte* ptr = buffer) + { + uint read; + HRESULT hr = stream.Read(ptr, (uint)buffer.Length, &read); + hr.ThrowOnFailure(); + return (int)read; + } + } + + public void Revert() + { + ObjectDisposedException.ThrowIf(disposed, this); + + stream.Revert(); + } + + public override unsafe long Seek(long offset, SeekOrigin origin) + { + ObjectDisposedException.ThrowIf(disposed, this); + + ulong pos; + stream.Seek(offset, origin, &pos); + return (long)pos; + } + + public override void SetLength(long value) + { + ObjectDisposedException.ThrowIf(disposed, this); + + stream.SetSize((ulong)value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(disposed, this); + + ReadOnlySpan slice = buffer.AsSpan(offset, count); + Write(slice); + } + + public override unsafe void Write(ReadOnlySpan buffer) + { + ObjectDisposedException.ThrowIf(disposed, this); + + fixed (byte* ptr = buffer) + { + uint written; + HRESULT result = stream.Write(ptr, (uint)buffer.Length, &written); + result.ThrowOnFailure(); + } + } + + // Properties + public override bool CanRead { get; } + + public override bool CanSeek => true; + + public override bool CanWrite { get; } + + public override long Length + { + get + { + ObjectDisposedException.ThrowIf(disposed, this); + + return (long)Stat.cbSize; + } + } + + public override unsafe long Position + { + get + { + ObjectDisposedException.ThrowIf(disposed, this); + + ulong pos; + stream.Seek(0L, SeekOrigin.Current, &pos); + return (long)pos; + } + + set + { + ObjectDisposedException.ThrowIf(disposed, this); + + stream.Seek(value, SeekOrigin.Begin, null); + } + } + + public Guid Id + { + get + { + ObjectDisposedException.ThrowIf(disposed, this); + + HRESULT hr = PInvoke.ReadClassStm(stream, out Guid guid); + hr.ThrowOnFailure(); + return guid; + } + + set + { + ObjectDisposedException.ThrowIf(disposed, this); + + HRESULT hr = PInvoke.WriteClassStm(stream, value); + hr.ThrowOnFailure(); + } + } + + internal unsafe STATSTG Stat + { + get + { + STATSTG stat; + stream.Stat(&stat, STATFLAG.STATFLAG_NONAME); + return stat; + } + } +} diff --git a/StructuredStorage/StructuredStorage.csproj b/StructuredStorage/StructuredStorage.csproj new file mode 100644 index 00000000..40393c66 --- /dev/null +++ b/StructuredStorage/StructuredStorage.csproj @@ -0,0 +1,18 @@ + + + + net8.0-windows + enable + enable + 12.0 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + From 1e397634be70432cbab08ed7f1fdc2e61f0eb2f5 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 13:33:47 +1300 Subject: [PATCH 080/114] Fix leave open flag --- OpenMcdf3/IOContext.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 3c13816b..a6c61aee 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -14,6 +14,7 @@ enum IOContextFlags internal sealed class IOContext : IDisposable { readonly Stream stream; + readonly IOContextFlags contextFlags; readonly CfbBinaryWriter? writer; readonly TransactedStream? transactedStream; MiniFat? miniFat; @@ -77,6 +78,7 @@ public FatStream MiniStream public IOContext(Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) { this.stream = stream; + this.contextFlags = contextFlags; using CfbBinaryReader reader = new(stream); Header = contextFlags.HasFlag(IOContextFlags.Create) ? new(version) : reader.ReadHeader(); @@ -130,6 +132,9 @@ public void Dispose() Fat.Dispose(); writer?.Dispose(); Reader.Dispose(); + transactedStream?.Dispose(); + if (!contextFlags.HasFlag(IOContextFlags.LeaveOpen)) + stream.Dispose(); IsDisposed = true; } } From 089876e07ec460483b0b4a9de4213e37dcf44532 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 14:33:43 +1300 Subject: [PATCH 081/114] Fix warnings --- OpenMcdf3/CfbBinaryReader.cs | 7 +- OpenMcdf3/CfbBinaryWriter.cs | 12 +- OpenMcdf3/CfbStream.cs | 2 +- OpenMcdf3/CompilerServices.cs | 9 -- OpenMcdf3/DirectoryEntryEnumerator.cs | 1 - OpenMcdf3/DirectoryTreeEnumerator.cs | 1 - OpenMcdf3/FatChainEnumerator.cs | 8 +- OpenMcdf3/FatStream.cs | 11 +- OpenMcdf3/Header.cs | 10 +- OpenMcdf3/MiniFatStream.cs | 2 +- OpenMcdf3/OpenMcdf3.csproj | 12 +- OpenMcdf3/StreamExtensions.cs | 6 +- OpenMcdf3/System/.editorconfig | 2 + OpenMcdf3/System/.globalconfig | 3 + OpenMcdf3/System/CompilerServices.cs | 5 + OpenMcdf3/System/Index.cs | 167 ++++++++++++++++++++ OpenMcdf3/System/NullableAttributes.cs | 201 +++++++++++++++++++++++++ OpenMcdf3/System/Range.cs | 128 ++++++++++++++++ OpenMcdf3/TransactedStream.cs | 4 +- 19 files changed, 549 insertions(+), 42 deletions(-) delete mode 100644 OpenMcdf3/CompilerServices.cs create mode 100644 OpenMcdf3/System/.editorconfig create mode 100644 OpenMcdf3/System/.globalconfig create mode 100644 OpenMcdf3/System/CompilerServices.cs create mode 100644 OpenMcdf3/System/Index.cs create mode 100644 OpenMcdf3/System/NullableAttributes.cs create mode 100644 OpenMcdf3/System/Range.cs diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index 51cbf2ae..5ffdb220 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -1,5 +1,8 @@ -using System.Buffers; -using System.Text; +using System.Text; + +#if NETSTANDARD2_0 +using System.Buffers; +#endif namespace OpenMcdf3; diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf3/CfbBinaryWriter.cs index 5ff8c4a6..6b4f9ba4 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf3/CfbBinaryWriter.cs @@ -7,8 +7,6 @@ namespace OpenMcdf3; /// internal sealed class CfbBinaryWriter : BinaryWriter { - readonly byte[] buffer = new byte[DirectoryEntry.NameFieldLength]; - public CfbBinaryWriter(Stream input) : base(input, Encoding.Unicode, true) { @@ -20,7 +18,7 @@ public long Position set => BaseStream.Position = value; } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public override void Write(ReadOnlySpan buffer) => BaseStream.Write(buffer); @@ -28,13 +26,13 @@ public long Position public void Write(in Guid value) { -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if NETSTANDARD2_0 || NETFRAMEWORK + byte[] bytes = value.ToByteArray(); + Write(bytes); +#else Span localBuffer = stackalloc byte[16]; value.TryWriteBytes(localBuffer); Write(localBuffer); -#else - byte[] bytes = value.ToByteArray(); - Write(bytes); #endif } diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index 933ef167..f030356b 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -94,7 +94,7 @@ public override void Write(byte[] buffer, int offset, int count) stream.Write(buffer, offset, count); } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public override int Read(Span buffer) => stream.Read(buffer); diff --git a/OpenMcdf3/CompilerServices.cs b/OpenMcdf3/CompilerServices.cs deleted file mode 100644 index 81332ada..00000000 --- a/OpenMcdf3/CompilerServices.cs +++ /dev/null @@ -1,9 +0,0 @@ -#if NETSTANDARD2_0 || NETSTANDARD2_1 - -namespace System.Runtime.CompilerServices; - -internal class IsExternalInit -{ -} - -#endif \ No newline at end of file diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index 05880a6e..fe979d06 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Diagnostics; namespace OpenMcdf3; diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index 91b93b9d..e11a4898 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Diagnostics; namespace OpenMcdf3; diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf3/FatChainEnumerator.cs index f97128a3..beb09245 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf3/FatChainEnumerator.cs @@ -29,8 +29,6 @@ public void Dispose() fatEnumerator.Dispose(); } - public uint StartId => startId; - public Sector CurrentSector => new(Current.Value, ioContext.SectorSize); /// @@ -141,7 +139,7 @@ public uint Extend() /// /// Returns the ID of the first sector in the chain. /// - public void Extend(uint requiredChainLength) + public uint Extend(uint requiredChainLength) { uint chainLength = (uint)GetLength(); if (chainLength >= requiredChainLength) @@ -168,6 +166,7 @@ public void Extend(uint requiredChainLength) } length = requiredChainLength; + return startId; } public uint ExtendFrom(uint hintId) @@ -189,7 +188,7 @@ public uint ExtendFrom(uint hintId) return id; } - public void Shrink(uint requiredChainLength) + public uint Shrink(uint requiredChainLength) { uint chainLength = (uint)GetLength(); if (chainLength <= requiredChainLength) @@ -225,6 +224,7 @@ public void Shrink(uint requiredChainLength) #endif length = requiredChainLength; + return startId; } /// diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 05fe2591..652b76b4 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -1,6 +1,4 @@ -using System.IO; - -namespace OpenMcdf3; +namespace OpenMcdf3; /// /// Provides a for a stream object in a compound file./> @@ -152,11 +150,10 @@ public override void SetLength(long value) uint requiredChainLength = (uint)((value + ioContext.SectorSize - 1) / ioContext.SectorSize); if (value > ChainCapacity) - chain.Extend(requiredChainLength); + DirectoryEntry.StartSectorId = chain.Extend(requiredChainLength); else if (value <= ChainCapacity - ioContext.SectorSize) - chain.Shrink(requiredChainLength); + DirectoryEntry.StartSectorId = chain.Shrink(requiredChainLength); - DirectoryEntry.StartSectorId = chain.StartId; DirectoryEntry.StreamLength = value; isDirty = true; } @@ -206,7 +203,7 @@ public override void Write(byte[] buffer, int offset, int count) throw new InvalidOperationException($"End of FAT chain was reached"); } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public override int ReadByte() => this.ReadByteCore(); diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 7225028f..3c99d722 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -1,5 +1,4 @@ - -namespace OpenMcdf3; +namespace OpenMcdf3; /// /// The structure at the beginning of a compound file. @@ -160,5 +159,12 @@ public bool Equals(Header? other) && Difat.SequenceEqual(other.Difat); } + public override int GetHashCode() + { + return HashCode.Combine( + HashCode.Combine(CLSID, MinorVersion, MajorVersion, SectorShift, DirectorySectorCount, FatSectorCount, FirstDirectorySectorId, TransactionSignature), + HashCode.Combine(FirstMiniFatSectorId, MiniFatSectorCount, FirstDifatSectorId, DifatSectorCount, Difat)); + } + public override string ToString() => $"MajorVersion: {MajorVersion}, MinorVersion: {MinorVersion}, FirstDirectorySectorId: {FirstDirectorySectorId}, FirstMiniFatSectorId: {FirstMiniFatSectorId}"; } diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index cea8cdcf..b90ee021 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -195,7 +195,7 @@ public override void Write(byte[] buffer, int offset, int count) throw new InvalidOperationException($"End of mini FAT chain was reached."); } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public override int ReadByte() => this.ReadByteCore(); diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf3/OpenMcdf3.csproj index 48b852fb..884c3d20 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf3/OpenMcdf3.csproj @@ -2,14 +2,20 @@ netstandard2.0;net8.0 - 11.0 + 12.0 enable enable true - - + + + + + + + + diff --git a/OpenMcdf3/StreamExtensions.cs b/OpenMcdf3/StreamExtensions.cs index ec600f63..5d904c00 100644 --- a/OpenMcdf3/StreamExtensions.cs +++ b/OpenMcdf3/StreamExtensions.cs @@ -1,4 +1,6 @@ -using System.Buffers; +#if !NET7_0_OR_GREATER +using System.Buffers; +#endif namespace OpenMcdf3; @@ -38,7 +40,7 @@ public static void ReadExactly(this Stream stream, byte[] buffer, int offset, in #endif -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public static int ReadByteCore(this Stream stream) { diff --git a/OpenMcdf3/System/.editorconfig b/OpenMcdf3/System/.editorconfig new file mode 100644 index 00000000..e4aabe9b --- /dev/null +++ b/OpenMcdf3/System/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +dotnet_analyzer_diagnostic.severity = none \ No newline at end of file diff --git a/OpenMcdf3/System/.globalconfig b/OpenMcdf3/System/.globalconfig new file mode 100644 index 00000000..54c4c5b9 --- /dev/null +++ b/OpenMcdf3/System/.globalconfig @@ -0,0 +1,3 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories + +dotnet_analyzer_diagnostic.severity = none \ No newline at end of file diff --git a/OpenMcdf3/System/CompilerServices.cs b/OpenMcdf3/System/CompilerServices.cs new file mode 100644 index 00000000..dfcd73bb --- /dev/null +++ b/OpenMcdf3/System/CompilerServices.cs @@ -0,0 +1,5 @@ +namespace System.Runtime.CompilerServices; + +internal class IsExternalInit +{ +} diff --git a/OpenMcdf3/System/Index.cs b/OpenMcdf3/System/Index.cs new file mode 100644 index 00000000..52a556be --- /dev/null +++ b/OpenMcdf3/System/Index.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); +#else + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); +#endif + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} \ No newline at end of file diff --git a/OpenMcdf3/System/NullableAttributes.cs b/OpenMcdf3/System/NullableAttributes.cs new file mode 100644 index 00000000..08417f13 --- /dev/null +++ b/OpenMcdf3/System/NullableAttributes.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NETSTANDARD2_1 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +#endif + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated field or property member will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated field and property members will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} \ No newline at end of file diff --git a/OpenMcdf3/System/Range.cs b/OpenMcdf3/System/Range.cs new file mode 100644 index 00000000..8967a17a --- /dev/null +++ b/OpenMcdf3/System/Range.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#if NETSTANDARD2_0 || NETFRAMEWORK +#endif + +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) + { + span[0] = '^'; + pos = 1; + } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; + + if (End.IsFromEnd) + { + span[pos++] = '^'; + } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + ThrowArgumentOutOfRangeException(); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); + } + } +} \ No newline at end of file diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index 2c055837..b5bdfbc6 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -130,7 +130,7 @@ public void Revert() dirtySectorPositions.Clear(); } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) public override int ReadByte() => this.ReadByteCore(); @@ -141,7 +141,7 @@ public override int Read(Span buffer) int localCount = Math.Min(buffer.Length, remainingFromSector); Debug.Assert(localCount == buffer.Length); - Span slice = buffer.Slice(0, localCount); + Span slice = buffer[..localCount]; int read; if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) { From 760bbc46482a972e7dfd62fd9e0aea032e8c5b71 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 15:15:49 +1300 Subject: [PATCH 082/114] Improve benchmarks --- OpenMcdf3.Benchmarks/InMemory.cs | 86 ------------------- OpenMcdf3.Benchmarks/MemoryStreamIO.cs | 60 +++++++++++++ .../OpenMcdf3.Benchmarks.csproj | 3 +- OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs | 25 ++++++ .../StructuredStorageBenchmarks.cs | 26 ++++++ StructuredStorage/Storage.cs | 2 + 6 files changed, 115 insertions(+), 87 deletions(-) delete mode 100644 OpenMcdf3.Benchmarks/InMemory.cs create mode 100644 OpenMcdf3.Benchmarks/MemoryStreamIO.cs create mode 100644 OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs create mode 100644 OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs diff --git a/OpenMcdf3.Benchmarks/InMemory.cs b/OpenMcdf3.Benchmarks/InMemory.cs deleted file mode 100644 index 209088ab..00000000 --- a/OpenMcdf3.Benchmarks/InMemory.cs +++ /dev/null @@ -1,86 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; - -namespace OpenMcdf3.Benchmark; - -[ShortRunJob] -[CsvExporter] -[HtmlExporter] -[MarkdownExporter] -//[DryCoreJob] // I always forget this attribute, so please leave it commented out -[MemoryDiagnoser] -[Orderer(SummaryOrderPolicy.FastestToSlowest)] -public class InMemory : IDisposable -{ - bool inMemory = true; - private const int Kb = 1024; - private const int Mb = Kb * Kb; - private const string storageName = "MyStorage"; - private const string streamName = "MyStream"; - - private Stream? readStream; - private Stream? writeStream; - - private byte[] buffer = Array.Empty(); - - [Params(512, Mb /*Kb, 4 * Kb, 128 * Kb, 256 * Kb, 512 * Kb,*/)] - public int BufferSize { get; set; } - - [Params(Mb /*, 8 * Mb, 64 * Mb, 128 * Mb*/)] - public int TotalStreamSize { get; set; } - - public void Dispose() - { - readStream?.Dispose(); - writeStream?.Dispose(); - } - - [GlobalSetup] - public void GlobalSetup() - { - buffer = new byte[BufferSize]; - readStream = inMemory ? new MemoryStream(2 * TotalStreamSize) : File.Create(Path.GetTempFileName()); - writeStream = inMemory ? new MemoryStream(2 * TotalStreamSize) : File.Create(Path.GetTempFileName()); - - using var storage = RootStorage.Create(readStream, Version.V3, StorageModeFlags.LeaveOpen); - using CfbStream stream = storage.CreateStream(streamName); - - int iterationCount = TotalStreamSize / BufferSize; - for (int iteration = 0; iteration < iterationCount; ++iteration) - stream.Write(buffer); - } - - [Benchmark] - public void Read() - { - using var compoundFile = RootStorage.Open(readStream); - using CfbStream cfStream = compoundFile.OpenStream(streamName); - long streamSize = cfStream.Length; - long position = 0L; - while (position < streamSize) - { - int read = cfStream.Read(buffer, 0, buffer.Length); - if (read <= 0) - throw new EndOfStreamException(); - position += read; - } - } - - [Benchmark] - public void Write() => WriteCore(StorageModeFlags.None); - - [Benchmark] - public void WriteTransacted() => WriteCore(StorageModeFlags.Transacted); - - void WriteCore(StorageModeFlags flags) - { - using var storage = RootStorage.Create(writeStream, Version.V3, flags); - Storage subStorage = storage.CreateStorage(storageName); - CfbStream stream = subStorage.CreateStream(streamName + 0); - - while (stream.Length < TotalStreamSize) - { - stream.Write(buffer, 0, buffer.Length); - } - } -} diff --git a/OpenMcdf3.Benchmarks/MemoryStreamIO.cs b/OpenMcdf3.Benchmarks/MemoryStreamIO.cs new file mode 100644 index 00000000..a7a5d7cc --- /dev/null +++ b/OpenMcdf3.Benchmarks/MemoryStreamIO.cs @@ -0,0 +1,60 @@ +using BenchmarkDotNet.Attributes; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[ShortRunJob] +[MemoryDiagnoser] +public class MemoryStreamIO : IDisposable +{ + const Version version = Version.V3; + + private MemoryStream? readStream; + private MemoryStream? writeStream; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + readStream?.Dispose(); + writeStream?.Dispose(); + } + + [GlobalSetup] + public void GlobalSetup() + { + buffer = new byte[BufferSize]; + readStream = new MemoryStream(2 * StreamLength); + writeStream = new MemoryStream(2 * StreamLength); + + using var storage = RootStorage.Create(readStream, version, StorageModeFlags.LeaveOpen); + using CfbStream stream = storage.CreateStream("Test"); + + int iterationCount = StreamLength / BufferSize; + for (int iteration = 0; iteration < iterationCount; ++iteration) + stream.Write(buffer); + } + + [Benchmark] + public void Read() => OpenMcdfBenchmarks.ReadStream(readStream!, buffer); + + [Benchmark] + public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen, buffer, StreamLength); + + [Benchmark] + public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength); + +#if WINDOWS + [Benchmark] + public void ReadStructuredStorage() => StructuredStorageBenchmarks.ReadStream(readStream!, buffer); + + [Benchmark] + public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(version, StorageModeFlags.LeaveOpen, buffer, StreamLength); +#endif +} diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj index 209959c3..b8f832e4 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj +++ b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0-windows 11.0 Exe enable @@ -14,6 +14,7 @@ + diff --git a/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs b/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs new file mode 100644 index 00000000..da4bd78c --- /dev/null +++ b/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs @@ -0,0 +1,25 @@ +namespace OpenMcdf3.Benchmarks; + +internal static class OpenMcdfBenchmarks +{ + public static void ReadStream(Stream stream, byte[] buffer) + { + using var storage = RootStorage.Open(stream, StorageModeFlags.LeaveOpen); + using CfbStream cfbStream = storage.OpenStream("Test"); + while (cfbStream.Position < cfbStream.Length) + { + int read = cfbStream.Read(buffer, 0, buffer.Length); + if (read <= 0) + throw new EndOfStreamException(); + } + } + + public static void WriteStream(Stream stream, Version version, StorageModeFlags flags, byte[] buffer, long streamLength) + { + using var storage = RootStorage.Create(stream, version, flags); + using CfbStream cfbStream = storage.CreateStream("Test"); + + while (stream.Length < streamLength) + stream.Write(buffer, 0, buffer.Length); + } +} diff --git a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs new file mode 100644 index 00000000..0ad07e0c --- /dev/null +++ b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs @@ -0,0 +1,26 @@ +namespace OpenMcdf3.Benchmarks; + +internal class StructuredStorageBenchmarks +{ + public static void ReadStream(MemoryStream stream, byte[] buffer) + { + using var storage = StructuredStorage.Storage.Open(stream); + using StructuredStorage.Stream storageStream = storage.OpenStream("Test"); + + while (storageStream.Position < storageStream.Length) + { + int read = stream.Read(buffer, 0, buffer.Length); + if (read <= 0) + throw new EndOfStreamException(); + } + } + + public static void WriteInMemory(Version version, StorageModeFlags flags, byte[] buffer, long streamLength) + { + using var storage = StructuredStorage.Storage.CreateInMemory((int)streamLength * 2); + using StructuredStorage.Stream stream = storage.CreateStream("Test"); + + while (stream.Length < streamLength) + stream.Write(buffer, 0, buffer.Length); + } +} diff --git a/StructuredStorage/Storage.cs b/StructuredStorage/Storage.cs index 369df70b..ee9ccfa7 100644 --- a/StructuredStorage/Storage.cs +++ b/StructuredStorage/Storage.cs @@ -185,6 +185,8 @@ public static Storage CreateInMemory(int capacity) public static unsafe Storage Open(MemoryStream stream, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) { + stream.Position = 0; + LockBytes lockBytes = new(stream); HRESULT hr = PInvoke.StgOpenStorageOnILockBytes(lockBytes.ILockBytes, null, (STGM)modes, null, out IStorage storage); hr.ThrowOnFailure(); From a1e5bdcfa3f7896dc0b8c6ad9bb70cf7b83436a9 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 16:27:46 +1300 Subject: [PATCH 083/114] Fix structured storage empty writes --- StructuredStorage/Stream.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/StructuredStorage/Stream.cs b/StructuredStorage/Stream.cs index 4b1df3f4..7495ebd1 100644 --- a/StructuredStorage/Stream.cs +++ b/StructuredStorage/Stream.cs @@ -109,6 +109,9 @@ public override unsafe void Write(ReadOnlySpan buffer) { ObjectDisposedException.ThrowIf(disposed, this); + if (buffer.Length == 0) + return; + fixed (byte* ptr = buffer) { uint written; From 47d7659410efa150358f60dd2c17d1d2715916e1 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 16:28:09 +1300 Subject: [PATCH 084/114] Add reference write read test --- OpenMcdf3.Tests/StreamTests.cs | 78 +++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf3.Tests/StreamTests.cs index 7a0f030f..d9237544 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf3.Tests/StreamTests.cs @@ -187,17 +187,91 @@ public void WriteThenRead(Version version, int length) stream.Write(expectedBuffer, 0, expectedBuffer.Length); } - using (var rootStorage = RootStorage.Open(memoryStream)) + byte[] actualBuffer = new byte[length]; + using (var rootStorage = RootStorage.Open(memoryStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream stream = rootStorage.OpenStream("TestStream"); + rootStorage.Validate(); + Assert.AreEqual(length, stream.Length); + + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } + +#if WINDOWS + using (var rootStorage = StructuredStorage.Storage.Open(memoryStream)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + using StructuredStorage.Stream stream = rootStorage.OpenStream("TestStream"); + Assert.AreEqual(length, stream.Length); + + stream.ReadExactly(actualBuffer); + CollectionAssert.AreEqual(expectedBuffer, actualBuffer); + } +#endif + } + +#if WINDOWS + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 63)] + [DataRow(Version.V3, 64)] // Mini-stream sector size + [DataRow(Version.V3, 65)] + [DataRow(Version.V3, 511)] + [DataRow(Version.V3, 512)] // Multiple stream sectors + [DataRow(Version.V3, 513)] + [DataRow(Version.V3, 4095)] + [DataRow(Version.V3, 4096)] + [DataRow(Version.V3, 4097)] + [DataRow(Version.V3, 128 * 512)] // Multiple FAT sectors + [DataRow(Version.V3, 1024 * 4096)] // Multiple FAT sectors + [DataRow(Version.V3, 7087616)] // First DIFAT chain + [DataRow(Version.V3, 2 * 7087616)] // Long DIFAT chain + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 63)] + [DataRow(Version.V4, 64)] // Mini-stream sector size + [DataRow(Version.V4, 65)] + [DataRow(Version.V4, 511)] + [DataRow(Version.V4, 512)] + [DataRow(Version.V4, 513)] + [DataRow(Version.V4, 4095)] + [DataRow(Version.V4, 4096)] // Multiple stream sectors + [DataRow(Version.V4, 4097)] + [DataRow(Version.V4, 1024 * 4096)] // Multiple FAT sectors (1024 * 4096) + [DataRow(Version.V4, 7087616 * 4)] // First DIFAT chain + [DataRow(Version.V4, 2 * 7087616 * 4)] // Long DIFAT chain + public void StructuredStorageWriteThenRead(Version version, int length) + { + // Fill with bytes equal to their position modulo 256 + byte[] expectedBuffer = new byte[length]; + for (int i = 0; i < length; i++) + expectedBuffer[i] = (byte)i; + + using MemoryStream memoryStream = new(); + string fileName = Path.GetTempFileName(); + File.Delete(fileName); + + using (var rootStorage = StructuredStorage.Storage.Create(fileName, StructuredStorage.StorageModes.AccessReadWrite | StructuredStorage.StorageModes.ShareExclusive, version == Version.V4)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + using StructuredStorage.Stream stream = rootStorage.CreateStream("TestStream"); + + stream.Write(expectedBuffer, 0, expectedBuffer.Length); + Assert.AreEqual(length, stream.Length); + } + + byte[] actualBuffer = new byte[length]; + using (var rootStorage = RootStorage.OpenRead(fileName)) { using CfbStream stream = rootStorage.OpenStream("TestStream"); rootStorage.Validate(); Assert.AreEqual(length, stream.Length); - byte[] actualBuffer = new byte[length]; stream.ReadExactly(actualBuffer); CollectionAssert.AreEqual(expectedBuffer, actualBuffer); } } +#endif [TestMethod] [DataRow(Version.V3, 0)] From 7f3e51bd99a6ea624575f91bb70888857032712d Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Fri, 8 Nov 2024 22:12:38 +1300 Subject: [PATCH 085/114] Fix reading reference streams --- OpenMcdf3/Fat.cs | 9 ++------- OpenMcdf3/FatSectorEnumerator.cs | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index adb5d6e5..97f4a85f 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -11,9 +11,7 @@ internal sealed class Fat : IEnumerable, IDisposable { private readonly IOContext ioContext; private readonly FatSectorEnumerator fatSectorEnumerator; - private readonly int DifatArrayElementCount; private readonly int FatElementsPerSector; - private readonly int DifatElementsPerSector; private readonly byte[] cachedSectorBuffer; Sector cachedSector = Sector.EndOfChain; private bool isDirty; @@ -22,8 +20,6 @@ public Fat(IOContext ioContext) { this.ioContext = ioContext; FatElementsPerSector = ioContext.SectorSize / sizeof(uint); - DifatElementsPerSector = FatElementsPerSector - 1; - DifatArrayElementCount = Header.DifatArrayLength * FatElementsPerSector; fatSectorEnumerator = new(ioContext); cachedSectorBuffer = new byte[ioContext.SectorSize]; } @@ -53,9 +49,8 @@ public uint this[uint key] uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) { - if (key < DifatArrayElementCount) - return (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); - return Header.DifatArrayLength + (uint)Math.DivRem(key - DifatArrayElementCount, DifatElementsPerSector, out elementIndex); + uint index = (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); + return index; } void CacheCurrentSector() diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index 16b7c8ef..e22994ea 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -9,6 +9,7 @@ namespace OpenMcdf3; internal sealed class FatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; + private readonly uint DifatElementsPerSector; private bool start = true; private uint index = uint.MaxValue; private uint difatSectorId; @@ -17,6 +18,7 @@ internal sealed class FatSectorEnumerator : IEnumerator public FatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; + DifatElementsPerSector = (uint)((ioContext.SectorSize / sizeof(uint)) - 1); difatSectorId = ioContext.Header.FirstDifatSectorId; } @@ -66,11 +68,18 @@ public bool MoveNext() } Sector difatSector = new(difatSectorId, ioContext.SectorSize); + uint elementIndex = (nextIndex - Header.DifatArrayLength) % DifatElementsPerSector; + ioContext.Reader.Position = difatSector.Position + elementIndex * sizeof(uint); + uint id2 = ioContext.Reader.ReadUInt32(); + index = nextIndex; - current = difatSector; + current = new Sector(id2, ioContext.SectorSize); - ioContext.Reader.Position = difatSector.EndPosition - sizeof(uint); - difatSectorId = ioContext.Reader.ReadUInt32(); + if (elementIndex == DifatElementsPerSector - 1) + { + ioContext.Reader.Position = difatSector.EndPosition - sizeof(uint); + difatSectorId = ioContext.Reader.ReadUInt32(); + } return true; } From ea58529d8623fc22dd0625db784e67d1b69dabc7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Sun, 10 Nov 2024 22:06:52 +1300 Subject: [PATCH 086/114] Add difat sector enumerator --- OpenMcdf3/DifatSectorEnumerator.cs | 131 +++++++++++++++++++++++++++++ OpenMcdf3/Fat.cs | 5 +- OpenMcdf3/FatSectorEnumerator.cs | 128 ++++++++++------------------ 3 files changed, 180 insertions(+), 84 deletions(-) create mode 100644 OpenMcdf3/DifatSectorEnumerator.cs diff --git a/OpenMcdf3/DifatSectorEnumerator.cs b/OpenMcdf3/DifatSectorEnumerator.cs new file mode 100644 index 00000000..4e306901 --- /dev/null +++ b/OpenMcdf3/DifatSectorEnumerator.cs @@ -0,0 +1,131 @@ +using System.Collections; +using System.Diagnostics; + +namespace OpenMcdf3; + +internal class DifatSectorEnumerator : IEnumerator +{ + private readonly IOContext ioContext; + public readonly uint DifatElementsPerSector; + bool start = true; + uint index = uint.MaxValue; + Sector current = Sector.EndOfChain; + private uint difatSectorId = SectorType.EndOfChain; + + public DifatSectorEnumerator(IOContext ioContext) + { + this.ioContext = ioContext; + DifatElementsPerSector = (uint)((ioContext.SectorSize / sizeof(uint)) - 1); + } + + /// + public void Dispose() + { + // IOContext is owned by parent + } + + /// + public Sector Current + { + get + { + if (current.Id == SectorType.EndOfChain) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return current; + } + } + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if (start) + { + start = false; + index = uint.MaxValue; + difatSectorId = ioContext.Header.FirstDifatSectorId; + } + + uint nextIndex = index + 1; + if (difatSectorId == SectorType.EndOfChain) + { + index = uint.MaxValue; + current = Sector.EndOfChain; + difatSectorId = SectorType.EndOfChain; + return false; + } + + current = new(difatSectorId, ioContext.SectorSize); + index = nextIndex; + ioContext.Reader.Position = current.EndPosition - sizeof(uint); + difatSectorId = ioContext.Reader.ReadUInt32(); + return true; + } + + public bool MoveTo(uint index) + { + if (index >= ioContext.Header.DifatSectorCount) + return false; + + if (start && !MoveNext()) + return false; + + if (index < this.index) + Reset(); + + while (start || this.index < index) + { + if (!MoveNext()) + return false; + } + + return true; + } + + /// + public void Reset() + { + start = true; + index = uint.MaxValue; + current = Sector.EndOfChain; + difatSectorId = SectorType.EndOfChain; + } + + public void Add() + { + Sector newDifatSector = new(ioContext.SectorCount, ioContext.SectorSize); + + Header header = ioContext.Header; + CfbBinaryWriter writer = ioContext.Writer; + if (header.FirstDifatSectorId == SectorType.EndOfChain) + { + header.FirstDifatSectorId = newDifatSector.Id; + } + else + { + bool ok = MoveTo(header.DifatSectorCount - 1); + if (!ok) + throw new InvalidOperationException("Failed to move to last DIFAT sector."); + + writer.Position = current.EndPosition - sizeof(uint); + writer.Write(newDifatSector.Id); + } + + writer.Position = newDifatSector.Position; + writer.Write(SectorDataCache.GetFatEntryData(newDifatSector.Length)); + writer.Position = newDifatSector.EndPosition - sizeof(uint); + writer.Write(SectorType.EndOfChain); + + ioContext.ExtendStreamLength(newDifatSector.EndPosition); + header.DifatSectorCount++; + + ioContext.Fat[newDifatSector.Id] = SectorType.Difat; + + start = false; + index = header.DifatSectorCount - 1; + current = newDifatSector; + difatSectorId = SectorType.EndOfChain; + } +} diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 97f4a85f..aed25162 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -135,7 +135,10 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) uint newSectorId = fatSectorEnumerator.Add(); // Next id must be free - bool ok = fatEnumerator.MoveTo(newSectorId + 1); + bool ok = fatEnumerator.MoveTo(newSectorId); + Debug.Assert(ok); + + ok = fatEnumerator.MoveNextFreeEntry(); Debug.Assert(ok); CacheCurrentSector(); diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf3/FatSectorEnumerator.cs index e22994ea..d4464514 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf3/FatSectorEnumerator.cs @@ -9,23 +9,22 @@ namespace OpenMcdf3; internal sealed class FatSectorEnumerator : IEnumerator { private readonly IOContext ioContext; - private readonly uint DifatElementsPerSector; + private readonly DifatSectorEnumerator difatSectorEnumerator; private bool start = true; private uint index = uint.MaxValue; - private uint difatSectorId; private Sector current = Sector.EndOfChain; public FatSectorEnumerator(IOContext ioContext) { this.ioContext = ioContext; - DifatElementsPerSector = (uint)((ioContext.SectorSize / sizeof(uint)) - 1); - difatSectorId = ioContext.Header.FirstDifatSectorId; + difatSectorEnumerator = new(ioContext); } /// public void Dispose() { // IOContext is owned by a parent + difatSectorEnumerator.Dispose(); } /// @@ -52,36 +51,7 @@ public bool MoveNext() } uint nextIndex = index + 1; - if (nextIndex < ioContext.Header.FatSectorCount && nextIndex < Header.DifatArrayLength) // Include the free entries - { - uint id = ioContext.Header.Difat[nextIndex]; - index = nextIndex; - current = new Sector(id, ioContext.SectorSize); - return true; - } - - if (difatSectorId == SectorType.EndOfChain) - { - index = uint.MaxValue; - current = Sector.EndOfChain; - return false; - } - - Sector difatSector = new(difatSectorId, ioContext.SectorSize); - uint elementIndex = (nextIndex - Header.DifatArrayLength) % DifatElementsPerSector; - ioContext.Reader.Position = difatSector.Position + elementIndex * sizeof(uint); - uint id2 = ioContext.Reader.ReadUInt32(); - - index = nextIndex; - current = new Sector(id2, ioContext.SectorSize); - - if (elementIndex == DifatElementsPerSector - 1) - { - ioContext.Reader.Position = difatSector.EndPosition - sizeof(uint); - difatSectorId = ioContext.Reader.ReadUInt32(); - } - - return true; + return MoveTo(nextIndex); } public bool IsAt(uint index) => !start && index == this.index; @@ -98,28 +68,39 @@ public bool MoveTo(uint index) if (index == this.index) return true; - if (index >= ioContext.Header.FatSectorCount + ioContext.Header.DifatSectorCount) + if (index >= ioContext.Header.FatSectorCount) { this.index = uint.MaxValue; current = Sector.EndOfChain; return false; } - if (this.index < Header.DifatArrayLength || index < this.index) + if (index < Header.DifatArrayLength) { - // Jump as close as possible - this.index = Math.Min(index, Header.DifatArrayLength - 1); - uint id = ioContext.Header.Difat[this.index]; - current = new(id, ioContext.SectorSize); - difatSectorId = ioContext.Header.FirstDifatSectorId; + this.index = index; + uint difatId = ioContext.Header.Difat[this.index]; + current = new(difatId, ioContext.SectorSize); + return true; + } + + if (index < this.index) + { + this.index = Header.DifatArrayLength; } - while (this.index < index) + uint difatSectorIndex = (uint)Math.DivRem(index - Header.DifatArrayLength, difatSectorEnumerator.DifatElementsPerSector, out long difatElementIndex); + if (!difatSectorEnumerator.MoveTo(difatSectorIndex)) { - if (!MoveNext()) - return false; + this.index = uint.MaxValue; + current = Sector.EndOfChain; + return false; } + Sector difatSector = difatSectorEnumerator.Current; + ioContext.Reader.Position = difatSector.Position + (difatElementIndex * sizeof(uint)); + uint id = ioContext.Reader.ReadUInt32(); + this.index = index; + current = new Sector(id, ioContext.SectorSize); return true; } @@ -128,8 +109,8 @@ public void Reset() { start = true; index = uint.MaxValue; - difatSectorId = ioContext.Header.FirstDifatSectorId; current = Sector.EndOfChain; + difatSectorEnumerator.Reset(); } (uint lastIndex, Sector lastSector) MoveToEnd() @@ -155,54 +136,35 @@ public uint Add() { // No FAT sectors are free, so add a new one Header header = ioContext.Header; - uint nextIndex = ioContext.Header.FatSectorCount + ioContext.Header.DifatSectorCount; - uint lastIndex = nextIndex - 1; - Sector newSector = new(ioContext.SectorCount, ioContext.SectorSize); + uint nextIndex = ioContext.Header.FatSectorCount; + Sector newFatSector = new(ioContext.SectorCount, ioContext.SectorSize); CfbBinaryWriter writer = ioContext.Writer; - writer.Position = newSector.Position; - writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); + writer.Position = newFatSector.Position; + writer.Write(SectorDataCache.GetFatEntryData(newFatSector.Length)); + ioContext.ExtendStreamLength(newFatSector.EndPosition); - uint sectorType; - if (nextIndex < Header.DifatArrayLength) - { - index = nextIndex; - current = newSector; - sectorType = SectorType.Fat; + header.FatSectorCount++; - header.Difat[nextIndex] = newSector.Id; - header.FatSectorCount++; + index = nextIndex; + current = newFatSector; - writer.Position = newSector.Position; - writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); + if (nextIndex < Header.DifatArrayLength) + { + header.Difat[nextIndex] = newFatSector.Id; } else { - bool ok = MoveTo(lastIndex); - Debug.Assert(ok); - - Sector lastSector = current; - writer.Position = lastSector.EndPosition - sizeof(uint); - writer.Write(newSector.Id); - - index = nextIndex; - current = newSector; - difatSectorId = newSector.Id; - sectorType = SectorType.Difat; - - writer.Position = newSector.Position; - writer.Write(SectorDataCache.GetFatEntryData(newSector.Length)); - - writer.Position = newSector.EndPosition - sizeof(uint); - writer.Write(SectorType.EndOfChain); + uint difatSectorIndex = (uint)Math.DivRem(nextIndex - Header.DifatArrayLength, difatSectorEnumerator.DifatElementsPerSector, out long difatElementIndex); + if (!difatSectorEnumerator.MoveTo(difatSectorIndex)) + difatSectorEnumerator.Add(); - // Chain the sector - if (header.FirstDifatSectorId == SectorType.EndOfChain) - header.FirstDifatSectorId = newSector.Id; - header.DifatSectorCount++; + Sector difatSector = difatSectorEnumerator.Current; + writer.Position = difatSector.Position + difatElementIndex * sizeof(uint); + writer.Write(newFatSector.Id); } - ioContext.Fat[newSector.Id] = sectorType; - return newSector.Id; + ioContext.Fat[newFatSector.Id] = SectorType.Fat; + return newFatSector.Id; } } From 2ac65882d9638cb8b7c1819c9c741d160cbe31f5 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 00:01:09 +1300 Subject: [PATCH 087/114] Improve benchmarks --- OpenMcdf3.Benchmarks/FileStreamRead.cs | 55 ++++++++++++++++++ .../FileStreamTransactedWrite.cs | 49 ++++++++++++++++ OpenMcdf3.Benchmarks/FileStreamWrite.cs | 48 +++++++++++++++ ...{MemoryStreamIO.cs => MemoryStreamRead.cs} | 22 +++---- .../MemoryStreamTransactedWrite.cs | 42 ++++++++++++++ OpenMcdf3.Benchmarks/MemoryStreamWrite.cs | 47 +++++++++++++++ OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs | 12 ++++ OpenMcdf3.Benchmarks/Program.cs | 4 +- .../StructuredStorageBenchmarks.cs | 58 ++++++++++++++++--- 9 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 OpenMcdf3.Benchmarks/FileStreamRead.cs create mode 100644 OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs create mode 100644 OpenMcdf3.Benchmarks/FileStreamWrite.cs rename OpenMcdf3.Benchmarks/{MemoryStreamIO.cs => MemoryStreamRead.cs} (63%) create mode 100644 OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs create mode 100644 OpenMcdf3.Benchmarks/MemoryStreamWrite.cs diff --git a/OpenMcdf3.Benchmarks/FileStreamRead.cs b/OpenMcdf3.Benchmarks/FileStreamRead.cs new file mode 100644 index 00000000..8acb0a6e --- /dev/null +++ b/OpenMcdf3.Benchmarks/FileStreamRead.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[MediumRunJob] +[MemoryDiagnoser] +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class FileStreamRead : IDisposable +{ + const Version version = Version.V3; + + private string readFileName = ""; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + File.Delete(readFileName); + } + + [GlobalSetup] + public void GlobalSetup() + { + readFileName = Path.GetTempFileName(); + + buffer = new byte[BufferSize]; + + using var storage = RootStorage.Create(readFileName, version, StorageModeFlags.LeaveOpen); + using CfbStream stream = storage.CreateStream("Test"); + + int iterationCount = StreamLength / BufferSize; + for (int iteration = 0; iteration < iterationCount; ++iteration) + stream.Write(buffer); + } + + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); + + [Benchmark] + public void Read() => OpenMcdfBenchmarks.ReadStream(readFileName!, buffer); + +#if WINDOWS + [Benchmark(Baseline = true)] + public void ReadStructuredStorage() => StructuredStorageBenchmarks.ReadStream(readFileName!, buffer); +#endif +} diff --git a/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs b/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs new file mode 100644 index 00000000..e26e1305 --- /dev/null +++ b/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[MediumRunJob] +[MemoryDiagnoser] +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class FileStreamTransactedWrite : IDisposable +{ + const Version version = Version.V3; + + private string writeFileName = ""; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + File.Delete(writeFileName); + } + + [GlobalSetup] + public void GlobalSetup() + { + writeFileName = Path.GetTempFileName(); + + buffer = new byte[BufferSize]; + } + + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); + + [Benchmark] + public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeFileName!, version, StorageModeFlags.None | StorageModeFlags.Transacted, buffer, StreamLength); + +#if WINDOWS + + [Benchmark(Baseline = true)] + public void WriteStructuredStorageTransacted() => StructuredStorageBenchmarks.WriteStream(writeFileName, version, StorageModeFlags.Transacted, buffer, StreamLength); +#endif +} diff --git a/OpenMcdf3.Benchmarks/FileStreamWrite.cs b/OpenMcdf3.Benchmarks/FileStreamWrite.cs new file mode 100644 index 00000000..d308a3c7 --- /dev/null +++ b/OpenMcdf3.Benchmarks/FileStreamWrite.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[MediumRunJob] +[MemoryDiagnoser] +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class FileStreamWrite : IDisposable +{ + const Version version = Version.V3; + + private string writeFileName = ""; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + File.Delete(writeFileName); + } + + [GlobalSetup] + public void GlobalSetup() + { + writeFileName = Path.GetTempFileName(); + + buffer = new byte[BufferSize]; + } + + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); + + [Benchmark] + public void Write() => OpenMcdfBenchmarks.WriteStream(writeFileName!, version, StorageModeFlags.None, buffer, StreamLength); + +#if WINDOWS + [Benchmark(Baseline = true)] + public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteStream(writeFileName, version, StorageModeFlags.None, buffer, StreamLength); +#endif +} diff --git a/OpenMcdf3.Benchmarks/MemoryStreamIO.cs b/OpenMcdf3.Benchmarks/MemoryStreamRead.cs similarity index 63% rename from OpenMcdf3.Benchmarks/MemoryStreamIO.cs rename to OpenMcdf3.Benchmarks/MemoryStreamRead.cs index a7a5d7cc..a050aabd 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamIO.cs +++ b/OpenMcdf3.Benchmarks/MemoryStreamRead.cs @@ -1,16 +1,18 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; using OpenMcdf3.Benchmarks; namespace OpenMcdf3.Benchmark; [ShortRunJob] [MemoryDiagnoser] -public class MemoryStreamIO : IDisposable +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class MemoryStreamRead : IDisposable { const Version version = Version.V3; private MemoryStream? readStream; - private MemoryStream? writeStream; private byte[] buffer = Array.Empty(); @@ -23,7 +25,6 @@ public class MemoryStreamIO : IDisposable public void Dispose() { readStream?.Dispose(); - writeStream?.Dispose(); } [GlobalSetup] @@ -31,7 +32,6 @@ public void GlobalSetup() { buffer = new byte[BufferSize]; readStream = new MemoryStream(2 * StreamLength); - writeStream = new MemoryStream(2 * StreamLength); using var storage = RootStorage.Create(readStream, version, StorageModeFlags.LeaveOpen); using CfbStream stream = storage.CreateStream("Test"); @@ -41,20 +41,14 @@ public void GlobalSetup() stream.Write(buffer); } - [Benchmark] - public void Read() => OpenMcdfBenchmarks.ReadStream(readStream!, buffer); - - [Benchmark] - public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen, buffer, StreamLength); + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); [Benchmark] - public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength); + public void Read() => OpenMcdfBenchmarks.ReadStream(readStream!, buffer); #if WINDOWS - [Benchmark] + [Benchmark(Baseline = true)] public void ReadStructuredStorage() => StructuredStorageBenchmarks.ReadStream(readStream!, buffer); - - [Benchmark] - public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(version, StorageModeFlags.LeaveOpen, buffer, StreamLength); #endif } diff --git a/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs b/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs new file mode 100644 index 00000000..2b0afaec --- /dev/null +++ b/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs @@ -0,0 +1,42 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[ShortRunJob] +[MemoryDiagnoser] +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class MemoryStreamTransactedWrite : IDisposable +{ + const Version version = Version.V3; + + private MemoryStream? writeStream; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + writeStream?.Dispose(); + } + + [GlobalSetup] + public void GlobalSetup() + { + buffer = new byte[BufferSize]; + writeStream = new MemoryStream(2 * StreamLength); + } + + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); + + [Benchmark] + public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength); +} diff --git a/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs b/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs new file mode 100644 index 00000000..87d848d3 --- /dev/null +++ b/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs @@ -0,0 +1,47 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using OpenMcdf3.Benchmarks; + +namespace OpenMcdf3.Benchmark; + +[ShortRunJob] +[MemoryDiagnoser] +[HideColumns(Column.AllocRatio)] +[MarkdownExporter] +public class MemoryStreamWrite : IDisposable +{ + const Version version = Version.V3; + + private MemoryStream? writeStream; + + private byte[] buffer = Array.Empty(); + + [Params(512, 1024 * 1024)] + public int BufferSize { get; set; } + + [Params(1024 * 1024)] + public int StreamLength { get; set; } + + public void Dispose() + { + writeStream?.Dispose(); + } + + [GlobalSetup] + public void GlobalSetup() + { + buffer = new byte[BufferSize]; + writeStream = new MemoryStream(2 * StreamLength); + } + + [GlobalCleanup] + public void GlobalCleanup() => Dispose(); + + [Benchmark] + public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen, buffer, StreamLength); + +#if WINDOWS + [Benchmark(Baseline = true)] + public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(version, StorageModeFlags.LeaveOpen, buffer, StreamLength); +#endif +} diff --git a/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs b/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs index da4bd78c..f4232b5c 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs +++ b/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs @@ -2,6 +2,12 @@ internal static class OpenMcdfBenchmarks { + public static void ReadStream(string fileName, byte[] buffer) + { + using FileStream fileStream = File.OpenRead(fileName); + ReadStream(fileStream, buffer); + } + public static void ReadStream(Stream stream, byte[] buffer) { using var storage = RootStorage.Open(stream, StorageModeFlags.LeaveOpen); @@ -14,6 +20,12 @@ public static void ReadStream(Stream stream, byte[] buffer) } } + public static void WriteStream(string fileName, Version version, StorageModeFlags flags, byte[] buffer, long streamLength) + { + using FileStream fileStream = File.Create(fileName); + WriteStream(fileStream, version, flags, buffer, streamLength); + } + public static void WriteStream(Stream stream, Version version, StorageModeFlags flags, byte[] buffer, long streamLength) { using var storage = RootStorage.Create(stream, version, flags); diff --git a/OpenMcdf3.Benchmarks/Program.cs b/OpenMcdf3.Benchmarks/Program.cs index 2707ca37..05756ebc 100644 --- a/OpenMcdf3.Benchmarks/Program.cs +++ b/OpenMcdf3.Benchmarks/Program.cs @@ -2,10 +2,10 @@ namespace OpenMcdf3.Benchmarks; -internal static class Program +internal sealed class Program { private static void Main(string[] args) { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(); } } diff --git a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs index 0ad07e0c..28b8b672 100644 --- a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs +++ b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs @@ -1,26 +1,70 @@ -namespace OpenMcdf3.Benchmarks; +using StructuredStorage; + +namespace OpenMcdf3.Benchmarks; internal class StructuredStorageBenchmarks { + public static void ReadStream(string fileName, byte[] buffer) + { + using var storage = StructuredStorage.Storage.Open(fileName); + using StructuredStorage.Stream storageStream = storage.OpenStream("Test"); + + long length = storageStream.Length; // Length lookup is expensive + long totalRead = 0; + while (totalRead < length) + { + int read = storageStream.Read(buffer, 0, buffer.Length); + if (read <= 0) + throw new EndOfStreamException($"Read past end of stream at {storageStream.Position}/{storageStream.Length}"); + totalRead += read; + } + } + public static void ReadStream(MemoryStream stream, byte[] buffer) { using var storage = StructuredStorage.Storage.Open(stream); using StructuredStorage.Stream storageStream = storage.OpenStream("Test"); - while (storageStream.Position < storageStream.Length) + long length = storageStream.Length; // Length lookup is expensive + long totalRead = 0; + while (totalRead < length) { - int read = stream.Read(buffer, 0, buffer.Length); + int read = storageStream.Read(buffer, 0, buffer.Length); if (read <= 0) - throw new EndOfStreamException(); + throw new EndOfStreamException($"Read past end of stream at {storageStream.Position}/{storageStream.Length}"); + totalRead += read; + } + } + + public static void WriteStream(string fileName, Version version, StorageModeFlags flags, byte[] buffer, long streamLength) + { + File.Delete(fileName); + + StorageModes modes = StorageModes.AccessReadWrite | StorageModes.ShareExclusive; + if (flags.HasFlag(StorageModeFlags.Transacted)) + modes |= StorageModes.Transacted; + + using var storage = StructuredStorage.Storage.Create(fileName, modes, version == Version.V4); + using StructuredStorage.Stream storageStream = storage.CreateStream("Test"); + + long totalWritten = 0; + while (totalWritten < streamLength) + { + storageStream.Write(buffer, 0, buffer.Length); + totalWritten += buffer.Length; } } public static void WriteInMemory(Version version, StorageModeFlags flags, byte[] buffer, long streamLength) { using var storage = StructuredStorage.Storage.CreateInMemory((int)streamLength * 2); - using StructuredStorage.Stream stream = storage.CreateStream("Test"); + using StructuredStorage.Stream storageStream = storage.CreateStream("Test"); - while (stream.Length < streamLength) - stream.Write(buffer, 0, buffer.Length); + long totalWritten = 0; + while (totalWritten < streamLength) + { + storageStream.Write(buffer, 0, buffer.Length); + totalWritten += buffer.Length; + } } } From cc2d502cce9af0425a4fab50a616f83f9b018c64 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 13:39:33 +1300 Subject: [PATCH 088/114] Delete transaction scratch file on dispose --- OpenMcdf3/IOContext.cs | 3 +++ OpenMcdf3/TransactedStream.cs | 51 ++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index a6c61aee..d47df376 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -132,7 +132,10 @@ public void Dispose() Fat.Dispose(); writer?.Dispose(); Reader.Dispose(); + string? overlayFileName = (transactedStream?.OverlayStream as FileStream)?.Name; transactedStream?.Dispose(); + if (overlayFileName is not null) + File.Delete(overlayFileName); if (!contextFlags.HasFlag(IOContextFlags.LeaveOpen)) stream.Dispose(); IsDisposed = true; diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf3/TransactedStream.cs index b5bdfbc6..42273a2a 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf3/TransactedStream.cs @@ -6,7 +6,6 @@ internal class TransactedStream : Stream { readonly IOContext ioContext; readonly Stream originalStream; - readonly Stream overlayStream; readonly Dictionary dirtySectorPositions = new(); readonly byte[] buffer; @@ -14,29 +13,31 @@ public TransactedStream(IOContext ioContext, Stream originalStream, Stream overl { this.ioContext = ioContext; this.originalStream = originalStream; - this.overlayStream = overlayStream; + OverlayStream = overlayStream; buffer = new byte[ioContext.SectorSize]; } protected override void Dispose(bool disposing) { // Original stream might be owned by the caller - overlayStream.Dispose(); + OverlayStream.Dispose(); base.Dispose(disposing); } + public Stream OverlayStream { get; } + public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => true; - public override long Length => overlayStream.Length; + public override long Length => OverlayStream.Length; public override long Position { get => originalStream.Position; set => originalStream.Position = value; } - public override void Flush() => overlayStream.Flush(); + public override void Flush() => OverlayStream.Flush(); public override int Read(byte[] buffer, int offset, int count) { @@ -52,8 +53,8 @@ public override int Read(byte[] buffer, int offset, int count) if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) { - overlayStream.Position = overlayPosition + sectorOffset; - read = overlayStream.Read(buffer, offset + totalRead, localCount); + OverlayStream.Position = overlayPosition + sectorOffset; + read = OverlayStream.Read(buffer, offset + totalRead, localCount); originalStream.Seek(read, SeekOrigin.Current); } else @@ -87,7 +88,7 @@ public override void Write(byte[] buffer, int offset, int count) bool added = false; if (!dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) { - overlayPosition = overlayStream.Length; + overlayPosition = OverlayStream.Length; dirtySectorPositions.Add(sectorId, overlayPosition); added = true; } @@ -99,14 +100,14 @@ public override void Write(byte[] buffer, int offset, int count) originalStream.Position = originalPosition - sectorOffset; originalStream.ReadExactly(this.buffer); - overlayStream.Position = overlayPosition; - overlayStream.Write(this.buffer, 0, this.buffer.Length); + OverlayStream.Position = overlayPosition; + OverlayStream.Write(this.buffer, 0, this.buffer.Length); } - overlayStream.Position = overlayPosition + sectorOffset; - overlayStream.Write(buffer, offset, localCount); - if (overlayStream.Length < overlayPosition + ioContext.SectorSize) - overlayStream.SetLength(overlayPosition + ioContext.SectorSize); + OverlayStream.Position = overlayPosition + sectorOffset; + OverlayStream.Write(buffer, offset, localCount); + if (OverlayStream.Length < overlayPosition + ioContext.SectorSize) + OverlayStream.SetLength(overlayPosition + ioContext.SectorSize); originalStream.Position = originalPosition + localCount; } @@ -114,8 +115,8 @@ public void Commit() { foreach (KeyValuePair entry in dirtySectorPositions) { - overlayStream.Position = entry.Value; - overlayStream.ReadExactly(buffer); + OverlayStream.Position = entry.Value; + OverlayStream.ReadExactly(buffer); originalStream.Position = entry.Key * ioContext.SectorSize; originalStream.Write(buffer, 0, buffer.Length); @@ -145,8 +146,8 @@ public override int Read(Span buffer) int read; if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) { - overlayStream.Position = overlayPosition + sectorOffset; - read = overlayStream.Read(slice); + OverlayStream.Position = overlayPosition + sectorOffset; + read = OverlayStream.Read(slice); originalStream.Seek(read, SeekOrigin.Current); } else @@ -170,7 +171,7 @@ public override void Write(ReadOnlySpan buffer) bool added = false; if (!dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) { - overlayPosition = overlayStream.Length; + overlayPosition = OverlayStream.Length; dirtySectorPositions.Add(sectorId, overlayPosition); added = true; } @@ -182,14 +183,14 @@ public override void Write(ReadOnlySpan buffer) originalStream.Position = originalPosition - sectorOffset; originalStream.ReadExactly(this.buffer); - overlayStream.Position = overlayPosition; - overlayStream.Write(this.buffer, 0, this.buffer.Length); + OverlayStream.Position = overlayPosition; + OverlayStream.Write(this.buffer, 0, this.buffer.Length); } - overlayStream.Position = overlayPosition + sectorOffset; - overlayStream.Write(buffer); - if (overlayStream.Length < overlayPosition + ioContext.SectorSize) - overlayStream.SetLength(overlayPosition + ioContext.SectorSize); + OverlayStream.Position = overlayPosition + sectorOffset; + OverlayStream.Write(buffer); + if (OverlayStream.Length < overlayPosition + ioContext.SectorSize) + OverlayStream.SetLength(overlayPosition + ioContext.SectorSize); originalStream.Position = originalPosition + localCount; } From f1391ff8f8341c2f95ef01bc98fe8decc759d957 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 16:02:02 +1300 Subject: [PATCH 089/114] Add flush with optional consolidation --- OpenMcdf3.Tests/StorageTests.cs | 26 ++++++++++++++- OpenMcdf3/Fat.cs | 31 +++++++++--------- OpenMcdf3/FatEnumerator.cs | 10 ------ OpenMcdf3/IOContext.cs | 30 +++++++++++------ OpenMcdf3/RootStorage.cs | 58 +++++++++++++++++++++++++++++++-- OpenMcdf3/Storage.cs | 19 +++++++++++ 6 files changed, 134 insertions(+), 40 deletions(-) diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 2390ee51..3c6897d7 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -217,10 +217,34 @@ public void DeleteStream(Version version) Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); } - using (var rootStorage = RootStorage.Create(memoryStream, version)) + using (var rootStorage = RootStorage.Open(memoryStream)) { rootStorage.Delete("Test"); Assert.AreEqual(0, rootStorage.EnumerateEntries().Count()); } } + + [TestMethod] + [DataRow(Version.V3)] + [DataRow(Version.V4)] + public void Consolidate(Version version) + { + byte[] buffer = new byte[4096]; + + using MemoryStream memoryStream = new(); + using var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen); + CfbStream stream = rootStorage.CreateStream("Test"); + Assert.AreEqual(1, rootStorage.EnumerateEntries().Count()); + + stream.Write(buffer, 0, buffer.Length); + rootStorage.Flush(); + + int originalMemoryStreamLength = (int)memoryStream.Length; + + rootStorage.Delete("Test"); + + rootStorage.Flush(true); + + Assert.IsTrue(originalMemoryStreamLength > memoryStream.Length); + } } diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index aed25162..f7da94a1 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -11,7 +11,7 @@ internal sealed class Fat : IEnumerable, IDisposable { private readonly IOContext ioContext; private readonly FatSectorEnumerator fatSectorEnumerator; - private readonly int FatElementsPerSector; + internal readonly int FatElementsPerSector; private readonly byte[] cachedSectorBuffer; Sector cachedSector = Sector.EndOfChain; private bool isDirty; @@ -145,7 +145,8 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) } FatEntry entry = fatEnumerator.Current; - ioContext.ExtendStreamLength(fatEnumerator.CurrentSector.EndPosition); + Sector sector = new(entry.Index, ioContext.SectorSize); + ioContext.ExtendStreamLength(sector.EndPosition); this[entry.Index] = SectorType.EndOfChain; return entry.Index; } @@ -156,27 +157,25 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) internal void WriteTrace(TextWriter writer) { - using FatEnumerator fatEnumerator = new(ioContext); - byte[] data = new byte[ioContext.SectorSize]; Stream baseStream = ioContext.Reader.BaseStream; writer.WriteLine("Start of FAT ================="); - while (fatEnumerator.MoveNext()) + foreach (FatEntry entry in this) { - FatEntry current = fatEnumerator.Current; - if (current.IsFree) + Sector sector = new(entry.Index, ioContext.SectorSize); + if (entry.IsFree) { - writer.WriteLine($"{current}"); + writer.WriteLine($"{entry}"); } else { - baseStream.Position = fatEnumerator.CurrentSector.Position; + baseStream.Position = sector.Position; baseStream.ReadExactly(data, 0, data.Length); string hex = BitConverter.ToString(data); - writer.WriteLine($"{current}: {hex}"); + writer.WriteLine($"{entry}: {hex}"); } } @@ -185,15 +184,15 @@ internal void WriteTrace(TextWriter writer) internal void Validate() { - using FatEnumerator fatEnumerator = new(ioContext); - - while (fatEnumerator.MoveNext()) + foreach (FatEntry entry in this) { - FatEntry current = fatEnumerator.Current; - if (current.Value <= SectorType.Maximum && fatEnumerator.CurrentSector.EndPosition > ioContext.Length) + Sector sector = new(entry.Index, ioContext.SectorSize); + if (entry.Value <= SectorType.Maximum && sector.EndPosition > ioContext.Length) { - throw new FormatException($"FAT entry {current} is beyond the end of the stream."); + throw new FormatException($"FAT entry {entry} is beyond the end of the stream."); } } } + + internal long GetFreeSectorCount() => this.Count(entry => entry.IsFree); } diff --git a/OpenMcdf3/FatEnumerator.cs b/OpenMcdf3/FatEnumerator.cs index d64aebad..ee59bbb3 100644 --- a/OpenMcdf3/FatEnumerator.cs +++ b/OpenMcdf3/FatEnumerator.cs @@ -22,16 +22,6 @@ public void Dispose() { } - public Sector CurrentSector - { - get - { - if (index == uint.MaxValue) - throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); - return new(index, ioContext.SectorSize); - } - } - /// public FatEntry Current { diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index d47df376..992a9fbc 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -13,7 +13,6 @@ enum IOContextFlags /// internal sealed class IOContext : IDisposable { - readonly Stream stream; readonly IOContextFlags contextFlags; readonly CfbBinaryWriter? writer; readonly TransactedStream? transactedStream; @@ -22,6 +21,8 @@ internal sealed class IOContext : IDisposable public Header Header { get; } + public Stream BaseStream { get; } + public CfbBinaryReader Reader { get; } public CfbBinaryWriter Writer @@ -75,9 +76,11 @@ public FatStream MiniStream public uint SectorCount => (uint)Math.Max(0, (Length - SectorSize) / SectorSize); // TODO: Check + bool isDirty; + public IOContext(Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) { - this.stream = stream; + BaseStream = stream; this.contextFlags = contextFlags; using CfbBinaryReader reader = new(stream); @@ -119,12 +122,7 @@ public void Dispose() { if (!IsDisposed) { - if (writer is not null && transactedStream is null) - { - // Ensure the stream is as long as expected - stream.SetLength(Length); - WriteHeader(); - } + Flush(); miniStream?.Dispose(); miniFat?.Dispose(); @@ -137,15 +135,27 @@ public void Dispose() if (overlayFileName is not null) File.Delete(overlayFileName); if (!contextFlags.HasFlag(IOContextFlags.LeaveOpen)) - stream.Dispose(); + BaseStream.Dispose(); IsDisposed = true; } } + public void Flush() + { + if (isDirty && writer is not null && transactedStream is null) + { + // Ensure the stream is as long as expected + BaseStream.SetLength(Length); + WriteHeader(); + isDirty = false; + } + } + public void ExtendStreamLength(long length) { if (Length < length) Length = length; + isDirty = true; } public void WriteHeader() @@ -171,7 +181,7 @@ public void Commit() public void Revert() { if (writer is null || transactedStream is null) - throw new InvalidOperationException("Cannot commit non-transacted storage."); + throw new InvalidOperationException("Cannot revert non-transacted storage."); transactedStream.Revert(); } diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index 3d452d30..a84eca03 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -20,6 +20,8 @@ public enum StorageModeFlags /// public sealed class RootStorage : Storage, IDisposable { + readonly StorageModeFlags storageModeFlags; + public static RootStorage Create(string fileName, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) { FileStream stream = File.Create(fileName); @@ -39,7 +41,7 @@ public static RootStorage Create(Stream stream, Version version = Version.V3, St contextFlags |= IOContextFlags.Transacted; IOContext ioContext = new(stream, version, contextFlags); - return new RootStorage(ioContext); + return new RootStorage(ioContext, flags); } public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags flags = StorageModeFlags.None) @@ -66,23 +68,73 @@ public static RootStorage Open(Stream stream, StorageModeFlags flags = StorageMo contextFlags |= IOContextFlags.Transacted; IOContext ioContext = new(stream, Version.Unknown, contextFlags); - return new RootStorage(ioContext); + return new RootStorage(ioContext, flags); } - RootStorage(IOContext ioContext) + RootStorage(IOContext ioContext, StorageModeFlags storageModeFlags) : base(ioContext, ioContext.RootEntry) { + this.storageModeFlags = storageModeFlags; } public void Dispose() => ioContext?.Dispose(); + public void Flush(bool consolidate = false) + { + this.ThrowIfDisposed(ioContext.IsDisposed); + + ioContext.Flush(); + + if (consolidate) + Consolidate(); + } + + void Consolidate() + { + // TODO: Consolidate by defragmentation instead of copy + + Stream? destinationStream = null; + + try + { + if (ioContext.BaseStream is MemoryStream) + destinationStream = new MemoryStream((int)ioContext.BaseStream.Length); + else if (ioContext.BaseStream is FileStream) + destinationStream = File.Create(Path.GetTempFileName()); + else + throw new NotSupportedException("Unsupported stream type for consolidation."); + + using (RootStorage destinationStorage = Create(destinationStream, ioContext.Version, storageModeFlags)) + CopyTo(destinationStorage); + + ioContext.BaseStream.Position = 0; + destinationStream.Position = 0; + + destinationStream.CopyTo(ioContext.BaseStream); + ioContext.BaseStream.SetLength(destinationStream.Length); + } + catch + { + if (destinationStream is FileStream fs) + { + string fileName = fs.Name; + fs.Dispose(); + File.Delete(fileName); + } + } + } + public void Commit() { + this.ThrowIfDisposed(ioContext.IsDisposed); + ioContext.Commit(); } public void Revert() { + this.ThrowIfDisposed(ioContext.IsDisposed); + ioContext.Revert(); } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index 33cf3b76..b9c581b9 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -103,6 +103,25 @@ public CfbStream OpenStream(string name) return new CfbStream(ioContext, entry); } + public void CopyTo(Storage destination) + { + foreach (DirectoryEntry entry in EnumerateDirectoryEntries()) + { + if (entry.Type is StorageType.Storage) + { + Storage subSource = new(ioContext, entry); + Storage subDestination = destination.CreateStorage(entry.NameString); + subSource.CopyTo(subDestination); + } + else if (entry.Type is StorageType.Stream) + { + CfbStream stream = new(ioContext, entry); + CfbStream destinationStream = destination.CreateStream(entry.NameString); + stream.CopyTo(destinationStream); + } + } + } + public void Delete(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); From 5b12f1b2c9eb5e34aaacc733f927cfeb772d05e4 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 20:35:16 +1300 Subject: [PATCH 090/114] Add v4 to benchmarks --- OpenMcdf3.Benchmarks/FileStreamRead.cs | 8 ++++---- OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs | 10 +++++----- OpenMcdf3.Benchmarks/FileStreamWrite.cs | 10 +++++----- OpenMcdf3.Benchmarks/MemoryStreamRead.cs | 8 ++++---- OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs | 8 ++++---- OpenMcdf3.Benchmarks/MemoryStreamWrite.cs | 10 +++++----- OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs | 3 +++ 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/OpenMcdf3.Benchmarks/FileStreamRead.cs b/OpenMcdf3.Benchmarks/FileStreamRead.cs index 8acb0a6e..ed3b3e1d 100644 --- a/OpenMcdf3.Benchmarks/FileStreamRead.cs +++ b/OpenMcdf3.Benchmarks/FileStreamRead.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class FileStreamRead : IDisposable { - const Version version = Version.V3; - private string readFileName = ""; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -34,7 +34,7 @@ public void GlobalSetup() buffer = new byte[BufferSize]; - using var storage = RootStorage.Create(readFileName, version, StorageModeFlags.LeaveOpen); + using var storage = RootStorage.Create(readFileName, Version, StorageModeFlags.LeaveOpen); using CfbStream stream = storage.CreateStream("Test"); int iterationCount = StreamLength / BufferSize; diff --git a/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs b/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs index e26e1305..3f7b1a3d 100644 --- a/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs +++ b/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class FileStreamTransactedWrite : IDisposable { - const Version version = Version.V3; - private string writeFileName = ""; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -39,11 +39,11 @@ public void GlobalSetup() public void GlobalCleanup() => Dispose(); [Benchmark] - public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeFileName!, version, StorageModeFlags.None | StorageModeFlags.Transacted, buffer, StreamLength); + public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeFileName!, Version, StorageModeFlags.None | StorageModeFlags.Transacted, buffer, StreamLength); #if WINDOWS [Benchmark(Baseline = true)] - public void WriteStructuredStorageTransacted() => StructuredStorageBenchmarks.WriteStream(writeFileName, version, StorageModeFlags.Transacted, buffer, StreamLength); + public void WriteStructuredStorageTransacted() => StructuredStorageBenchmarks.WriteStream(writeFileName, Version, StorageModeFlags.Transacted, buffer, StreamLength); #endif } diff --git a/OpenMcdf3.Benchmarks/FileStreamWrite.cs b/OpenMcdf3.Benchmarks/FileStreamWrite.cs index d308a3c7..d5b0d8ce 100644 --- a/OpenMcdf3.Benchmarks/FileStreamWrite.cs +++ b/OpenMcdf3.Benchmarks/FileStreamWrite.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class FileStreamWrite : IDisposable { - const Version version = Version.V3; - private string writeFileName = ""; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -39,10 +39,10 @@ public void GlobalSetup() public void GlobalCleanup() => Dispose(); [Benchmark] - public void Write() => OpenMcdfBenchmarks.WriteStream(writeFileName!, version, StorageModeFlags.None, buffer, StreamLength); + public void Write() => OpenMcdfBenchmarks.WriteStream(writeFileName!, Version, StorageModeFlags.None, buffer, StreamLength); #if WINDOWS [Benchmark(Baseline = true)] - public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteStream(writeFileName, version, StorageModeFlags.None, buffer, StreamLength); + public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteStream(writeFileName, Version, StorageModeFlags.None, buffer, StreamLength); #endif } diff --git a/OpenMcdf3.Benchmarks/MemoryStreamRead.cs b/OpenMcdf3.Benchmarks/MemoryStreamRead.cs index a050aabd..56a40285 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamRead.cs +++ b/OpenMcdf3.Benchmarks/MemoryStreamRead.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class MemoryStreamRead : IDisposable { - const Version version = Version.V3; - private MemoryStream? readStream; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -33,7 +33,7 @@ public void GlobalSetup() buffer = new byte[BufferSize]; readStream = new MemoryStream(2 * StreamLength); - using var storage = RootStorage.Create(readStream, version, StorageModeFlags.LeaveOpen); + using var storage = RootStorage.Create(readStream, Version, StorageModeFlags.LeaveOpen); using CfbStream stream = storage.CreateStream("Test"); int iterationCount = StreamLength / BufferSize; diff --git a/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs b/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs index 2b0afaec..73c6ef87 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs +++ b/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class MemoryStreamTransactedWrite : IDisposable { - const Version version = Version.V3; - private MemoryStream? writeStream; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -38,5 +38,5 @@ public void GlobalSetup() public void GlobalCleanup() => Dispose(); [Benchmark] - public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength); + public void WriteTransacted() => OpenMcdfBenchmarks.WriteStream(writeStream!, Version, StorageModeFlags.LeaveOpen | StorageModeFlags.Transacted, buffer, StreamLength); } diff --git a/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs b/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs index 87d848d3..db669337 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs +++ b/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs @@ -10,12 +10,12 @@ namespace OpenMcdf3.Benchmark; [MarkdownExporter] public class MemoryStreamWrite : IDisposable { - const Version version = Version.V3; - private MemoryStream? writeStream; - private byte[] buffer = Array.Empty(); + [Params(Version.V3, Version.V4)] + public Version Version { get; set; } + [Params(512, 1024 * 1024)] public int BufferSize { get; set; } @@ -38,10 +38,10 @@ public void GlobalSetup() public void GlobalCleanup() => Dispose(); [Benchmark] - public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, version, StorageModeFlags.LeaveOpen, buffer, StreamLength); + public void Write() => OpenMcdfBenchmarks.WriteStream(writeStream!, Version, StorageModeFlags.LeaveOpen, buffer, StreamLength); #if WINDOWS [Benchmark(Baseline = true)] - public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(version, StorageModeFlags.LeaveOpen, buffer, StreamLength); + public void WriteStructuredStorage() => StructuredStorageBenchmarks.WriteInMemory(Version, StorageModeFlags.LeaveOpen, buffer, StreamLength); #endif } diff --git a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs index 28b8b672..c83cde10 100644 --- a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs +++ b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs @@ -57,6 +57,9 @@ public static void WriteStream(string fileName, Version version, StorageModeFlag public static void WriteInMemory(Version version, StorageModeFlags flags, byte[] buffer, long streamLength) { + if (version == Version.V4) + throw new NotSupportedException(); + using var storage = StructuredStorage.Storage.CreateInMemory((int)streamLength * 2); using StructuredStorage.Stream storageStream = storage.CreateStream("Test"); From 45432d3aaa691d6268737c16ff40f56155310738 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 20:42:10 +1300 Subject: [PATCH 091/114] Add validation for FAT/DIFAT sector count --- OpenMcdf3/Fat.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index f7da94a1..839d9c51 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -184,14 +184,23 @@ internal void WriteTrace(TextWriter writer) internal void Validate() { + long fatSectorCount = 0; + long difatSectorCount = 0; foreach (FatEntry entry in this) { Sector sector = new(entry.Index, ioContext.SectorSize); if (entry.Value <= SectorType.Maximum && sector.EndPosition > ioContext.Length) - { throw new FormatException($"FAT entry {entry} is beyond the end of the stream."); - } + if (entry.Value == SectorType.Fat) + fatSectorCount++; + if (entry.Value == SectorType.Difat) + difatSectorCount++; } + + if (ioContext.Header.FatSectorCount != fatSectorCount) + throw new FormatException($"FAT sector count mismatch. Expected: {ioContext.Header.FatSectorCount} Actual: {fatSectorCount}."); + if (ioContext.Header.DifatSectorCount != difatSectorCount) + throw new FormatException($"DIFAT sector count mismatch: Expected: {ioContext.Header.DifatSectorCount} Actual: {difatSectorCount}."); } internal long GetFreeSectorCount() => this.Count(entry => entry.IsFree); From d91bf30f4d4380358cffd01b479116fe2463ffd8 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 20:56:04 +1300 Subject: [PATCH 092/114] Trace free/used sectors --- OpenMcdf3/Fat.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf3/Fat.cs index 839d9c51..9e3a2b0a 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf3/Fat.cs @@ -163,15 +163,20 @@ internal void WriteTrace(TextWriter writer) writer.WriteLine("Start of FAT ================="); + long freeCount = 0; + long usedCount = 0; + foreach (FatEntry entry in this) { Sector sector = new(entry.Index, ioContext.SectorSize); if (entry.IsFree) { + freeCount++; writer.WriteLine($"{entry}"); } else { + usedCount++; baseStream.Position = sector.Position; baseStream.ReadExactly(data, 0, data.Length); string hex = BitConverter.ToString(data); @@ -180,6 +185,9 @@ internal void WriteTrace(TextWriter writer) } writer.WriteLine("End of FAT ==================="); + writer.WriteLine(); + writer.WriteLine($"Free sectors: {freeCount}"); + writer.WriteLine($"Used sectors: {usedCount}"); } internal void Validate() From 4d36a9ddd9f29afa7e5d71f2961ca1970a967c79 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 21:25:51 +1300 Subject: [PATCH 093/114] Improve root storage flag handling --- OpenMcdf3.Tests/StorageTests.cs | 2 +- OpenMcdf3/RootStorage.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 3c6897d7..1160186b 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -10,7 +10,7 @@ public sealed class StorageTests [DataRow("MultipleStorage4.cfs", 1)] public void Read(string fileName, long storageCount) { - using (var rootStorage = RootStorage.OpenRead(fileName, StorageModeFlags.LeaveOpen)) + using (var rootStorage = RootStorage.OpenRead(fileName)) { IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); Assert.AreEqual(storageCount, storageEntries.Count()); diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf3/RootStorage.cs index a84eca03..b50fb065 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf3/RootStorage.cs @@ -22,10 +22,18 @@ public sealed class RootStorage : Storage, IDisposable { readonly StorageModeFlags storageModeFlags; + private static void ThrowIfLeaveOpen(StorageModeFlags flags) + { + if (flags.HasFlag(StorageModeFlags.LeaveOpen)) + throw new ArgumentException($"{StorageModeFlags.LeaveOpen} is not valid for files"); + } + public static RootStorage Create(string fileName, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) { + ThrowIfLeaveOpen(flags); + FileStream stream = File.Create(fileName); - return Create(stream, version); + return Create(stream, version, flags); } public static RootStorage Create(Stream stream, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) @@ -46,14 +54,18 @@ public static RootStorage Create(Stream stream, Version version = Version.V3, St public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags flags = StorageModeFlags.None) { + ThrowIfLeaveOpen(flags); + FileStream stream = File.Open(fileName, mode); - return Open(stream); + return Open(stream, flags); } public static RootStorage OpenRead(string fileName, StorageModeFlags flags = StorageModeFlags.None) { + ThrowIfLeaveOpen(flags); + FileStream stream = File.OpenRead(fileName); - return Open(stream); + return Open(stream, flags); } public static RootStorage Open(Stream stream, StorageModeFlags flags = StorageModeFlags.None) From 0c596c094cb7c82607288bfbb2665d63f6778298 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 21:54:06 +1300 Subject: [PATCH 094/114] Rename Directories to DirectoryEntries --- OpenMcdf3/{Directories.cs => DirectoryEntries.cs} | 4 ++-- OpenMcdf3/DirectoryEntryEnumerator.cs | 4 ++-- OpenMcdf3/DirectoryTree.cs | 4 ++-- OpenMcdf3/DirectoryTreeEnumerator.cs | 4 ++-- OpenMcdf3/FatStream.cs | 2 +- OpenMcdf3/IOContext.cs | 12 ++++++------ OpenMcdf3/MiniFatStream.cs | 2 +- OpenMcdf3/Storage.cs | 6 +++--- 8 files changed, 19 insertions(+), 19 deletions(-) rename OpenMcdf3/{Directories.cs => DirectoryEntries.cs} (97%) diff --git a/OpenMcdf3/Directories.cs b/OpenMcdf3/DirectoryEntries.cs similarity index 97% rename from OpenMcdf3/Directories.cs rename to OpenMcdf3/DirectoryEntries.cs index f96513d9..e217737b 100644 --- a/OpenMcdf3/Directories.cs +++ b/OpenMcdf3/DirectoryEntries.cs @@ -1,13 +1,13 @@ namespace OpenMcdf3; -internal sealed class Directories : IDisposable +internal sealed class DirectoryEntries : IDisposable { private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; private readonly DirectoryEntryEnumerator directoryEntryEnumerator; private readonly int entriesPerSector; - public Directories(IOContext ioContext) + public DirectoryEntries(IOContext ioContext) { this.ioContext = ioContext; fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf3/DirectoryEntryEnumerator.cs index fe979d06..1e73a062 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf3/DirectoryEntryEnumerator.cs @@ -7,12 +7,12 @@ namespace OpenMcdf3; /// internal sealed class DirectoryEntryEnumerator : IEnumerator { - private readonly Directories directories; + private readonly DirectoryEntries directories; private bool start = true; private uint index = uint.MaxValue; private DirectoryEntry? current; - public DirectoryEntryEnumerator(Directories directories) + public DirectoryEntryEnumerator(DirectoryEntries directories) { this.directories = directories; } diff --git a/OpenMcdf3/DirectoryTree.cs b/OpenMcdf3/DirectoryTree.cs index c8077b9a..f3cfccec 100644 --- a/OpenMcdf3/DirectoryTree.cs +++ b/OpenMcdf3/DirectoryTree.cs @@ -11,10 +11,10 @@ internal enum RelationType Directory, } - private readonly Directories directories; + private readonly DirectoryEntries directories; private readonly DirectoryEntry root; - public DirectoryTree(Directories directories, DirectoryEntry root) + public DirectoryTree(DirectoryEntries directories, DirectoryEntry root) { this.directories = directories; this.root = root; diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf3/DirectoryTreeEnumerator.cs index e11a4898..9b1eae0c 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf3/DirectoryTreeEnumerator.cs @@ -7,12 +7,12 @@ namespace OpenMcdf3; /// internal sealed class DirectoryTreeEnumerator : IEnumerator { - private readonly Directories directories; + private readonly DirectoryEntries directories; private readonly DirectoryEntry root; private readonly Stack stack = new(); DirectoryEntry? current; - internal DirectoryTreeEnumerator(Directories directories, DirectoryEntry root) + internal DirectoryTreeEnumerator(DirectoryEntries directories, DirectoryEntry root) { this.directories = directories; this.root = root; diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf3/FatStream.cs index 652b76b4..bc0dd9a4 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf3/FatStream.cs @@ -63,7 +63,7 @@ public override void Flush() if (isDirty) { - ioContext.Directories.Write(DirectoryEntry); + ioContext.DirectoryEntries.Write(DirectoryEntry); isDirty = false; } diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf3/IOContext.cs index 992a9fbc..7966c611 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf3/IOContext.cs @@ -37,7 +37,7 @@ public CfbBinaryWriter Writer public Fat Fat { get; } - public Directories Directories { get; } + public DirectoryEntries DirectoryEntries { get; } public DirectoryEntry RootEntry { get; } @@ -102,19 +102,19 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I writer = new(actualStream); Fat = new(this); - Directories = new(this); + DirectoryEntries = new(this); if (contextFlags.HasFlag(IOContextFlags.Create)) { - RootEntry = Directories.CreateOrRecycleDirectoryEntry(); + RootEntry = DirectoryEntries.CreateOrRecycleDirectoryEntry(); RootEntry.RecycleRoot(); WriteHeader(); - Directories.Write(RootEntry); + DirectoryEntries.Write(RootEntry); } else { - RootEntry = Directories.GetDictionaryEntry(0); + RootEntry = DirectoryEntries.GetDictionaryEntry(0); } } @@ -126,7 +126,7 @@ public void Dispose() miniStream?.Dispose(); miniFat?.Dispose(); - Directories.Dispose(); + DirectoryEntries.Dispose(); Fat.Dispose(); writer?.Dispose(); Reader.Dispose(); diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf3/MiniFatStream.cs index b90ee021..b3a1beea 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf3/MiniFatStream.cs @@ -55,7 +55,7 @@ public override void Flush() if (isDirty) { - ioContext.Directories.Write(DirectoryEntry); + ioContext.DirectoryEntries.Write(DirectoryEntry); isDirty = false; } diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index b9c581b9..e7a77572 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -16,7 +16,7 @@ internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) throw new ArgumentException("DirectoryEntry must be a Storage or Root.", nameof(directoryEntry)); this.ioContext = ioContext; - directoryTree = new(ioContext.Directories, directoryEntry); + directoryTree = new(ioContext.DirectoryEntries, directoryEntry); DirectoryEntry = directoryEntry; } @@ -38,7 +38,7 @@ public IEnumerable EnumerateEntries(StorageType type) IEnumerable EnumerateDirectoryEntries() { - using DirectoryTreeEnumerator treeEnumerator = new(ioContext.Directories, DirectoryEntry); + using DirectoryTreeEnumerator treeEnumerator = new(ioContext.DirectoryEntries, DirectoryEntry); while (treeEnumerator.MoveNext()) { yield return treeEnumerator.Current; @@ -50,7 +50,7 @@ IEnumerable EnumerateDirectoryEntries(StorageType type) => Enume DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) { - DirectoryEntry entry = ioContext.Directories.CreateOrRecycleDirectoryEntry(); + DirectoryEntry entry = ioContext.DirectoryEntries.CreateOrRecycleDirectoryEntry(); entry.Recycle(storageType, name); directoryTree.Add(entry); return entry; From 5e873e8df5d8a289d028476c50e6eb24418a8b71 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 22:43:27 +1300 Subject: [PATCH 095/114] Port OpenMcdf Extensions (OLE) --- OpenMcdf.Ole/CodePages.cs | 6 + OpenMcdf.Ole/Common.cs | 8 + OpenMcdf.Ole/DefaultPropertyFactory.cs | 7 + OpenMcdf.Ole/DictionaryEntry.cs | 61 ++ OpenMcdf.Ole/DictionaryProperty.cs | 132 ++++ .../DocumentSummaryInfoPropertyFactory.cs | 16 + OpenMcdf.Ole/IBinarySerializable.cs | 7 + OpenMcdf.Ole/IDictionaryProperty.cs | 6 + OpenMcdf.Ole/IProperty.cs | 21 + OpenMcdf.Ole/ITypedPropertyValue.cs | 21 + OpenMcdf.Ole/Identifiers.cs | 27 + OpenMcdf.Ole/OlePropertiesContainer.cs | 316 +++++++++ OpenMcdf.Ole/OleProperty.cs | 64 ++ OpenMcdf.Ole/OpenMcdf.Ole.csproj | 16 + OpenMcdf.Ole/PropertyContext.cs | 14 + OpenMcdf.Ole/PropertyFactory.cs | 665 ++++++++++++++++++ OpenMcdf.Ole/PropertyIdentifierAndOffset.cs | 19 + OpenMcdf.Ole/PropertySet.cs | 32 + OpenMcdf.Ole/PropertySetStream.cs | 235 +++++++ OpenMcdf.Ole/ProperyIdentifiers.cs | 48 ++ OpenMcdf.Ole/TypedPropertyValue.cs | 155 ++++ OpenMcdf.Ole/VTPropertyType.cs | 44 ++ OpenMcdf.Ole/WellKnownFormatIdentifiers.cs | 11 + .../StructuredStorageBenchmarks.cs | 2 +- OpenMcdf3.sln | 6 + 25 files changed, 1938 insertions(+), 1 deletion(-) create mode 100644 OpenMcdf.Ole/CodePages.cs create mode 100644 OpenMcdf.Ole/Common.cs create mode 100644 OpenMcdf.Ole/DefaultPropertyFactory.cs create mode 100644 OpenMcdf.Ole/DictionaryEntry.cs create mode 100644 OpenMcdf.Ole/DictionaryProperty.cs create mode 100644 OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs create mode 100644 OpenMcdf.Ole/IBinarySerializable.cs create mode 100644 OpenMcdf.Ole/IDictionaryProperty.cs create mode 100644 OpenMcdf.Ole/IProperty.cs create mode 100644 OpenMcdf.Ole/ITypedPropertyValue.cs create mode 100644 OpenMcdf.Ole/Identifiers.cs create mode 100644 OpenMcdf.Ole/OlePropertiesContainer.cs create mode 100644 OpenMcdf.Ole/OleProperty.cs create mode 100644 OpenMcdf.Ole/OpenMcdf.Ole.csproj create mode 100644 OpenMcdf.Ole/PropertyContext.cs create mode 100644 OpenMcdf.Ole/PropertyFactory.cs create mode 100644 OpenMcdf.Ole/PropertyIdentifierAndOffset.cs create mode 100644 OpenMcdf.Ole/PropertySet.cs create mode 100644 OpenMcdf.Ole/PropertySetStream.cs create mode 100644 OpenMcdf.Ole/ProperyIdentifiers.cs create mode 100644 OpenMcdf.Ole/TypedPropertyValue.cs create mode 100644 OpenMcdf.Ole/VTPropertyType.cs create mode 100644 OpenMcdf.Ole/WellKnownFormatIdentifiers.cs diff --git a/OpenMcdf.Ole/CodePages.cs b/OpenMcdf.Ole/CodePages.cs new file mode 100644 index 00000000..1f5a22c7 --- /dev/null +++ b/OpenMcdf.Ole/CodePages.cs @@ -0,0 +1,6 @@ +namespace OpenMcdf.Ole; + +internal static class CodePages +{ + public const int WinUnicode = 0x04B0; +} diff --git a/OpenMcdf.Ole/Common.cs b/OpenMcdf.Ole/Common.cs new file mode 100644 index 00000000..3ee12e48 --- /dev/null +++ b/OpenMcdf.Ole/Common.cs @@ -0,0 +1,8 @@ +namespace OpenMcdf.Ole; + +public enum PropertyDimensions +{ + IsScalar, + IsVector, + IsArray +} diff --git a/OpenMcdf.Ole/DefaultPropertyFactory.cs b/OpenMcdf.Ole/DefaultPropertyFactory.cs new file mode 100644 index 00000000..4b432a7e --- /dev/null +++ b/OpenMcdf.Ole/DefaultPropertyFactory.cs @@ -0,0 +1,7 @@ +namespace OpenMcdf.Ole; + +// The default property factory. +internal sealed class DefaultPropertyFactory : PropertyFactory +{ + public static PropertyFactory Instance { get; } = new DefaultPropertyFactory(); +} diff --git a/OpenMcdf.Ole/DictionaryEntry.cs b/OpenMcdf.Ole/DictionaryEntry.cs new file mode 100644 index 00000000..68c04b6e --- /dev/null +++ b/OpenMcdf.Ole/DictionaryEntry.cs @@ -0,0 +1,61 @@ +using System.Text; + +namespace OpenMcdf.Ole; + +public class DictionaryEntry +{ + readonly int codePage; + + public DictionaryEntry(int codePage) + { + this.codePage = codePage; + } + + public uint PropertyIdentifier { get; set; } + public int Length { get; set; } + public string Name => GetName(); + + private byte[] nameBytes; + + public void Read(BinaryReader br) + { + PropertyIdentifier = br.ReadUInt32(); + Length = br.ReadInt32(); + + if (codePage != CodePages.WinUnicode) + { + nameBytes = br.ReadBytes(Length); + } + else + { + nameBytes = br.ReadBytes(Length << 1); + + int m = Length * 2 % 4; + if (m > 0) + br.ReadBytes(m); + } + } + + public void Write(BinaryWriter bw) + { + bw.Write(PropertyIdentifier); + bw.Write(Length); + bw.Write(nameBytes); + + //if (codePage == CP_WINUNICODE) + // int m = Length % 4; + + //if (m > 0) + // for (int i = 0; i < m; i++) + // bw.Write((byte)m); + } + + private string GetName() + { + string result = Encoding.GetEncoding(codePage).GetString(nameBytes); + + result = result.Trim('\0'); + + return result; + } +} diff --git a/OpenMcdf.Ole/DictionaryProperty.cs b/OpenMcdf.Ole/DictionaryProperty.cs new file mode 100644 index 00000000..1089307c --- /dev/null +++ b/OpenMcdf.Ole/DictionaryProperty.cs @@ -0,0 +1,132 @@ +using System.Text; + +namespace OpenMcdf.Ole; + +public class DictionaryProperty : IDictionaryProperty +{ + private readonly int codePage; + + public DictionaryProperty(int codePage) + { + this.codePage = codePage; + entries = new Dictionary(); + } + + public PropertyType PropertyType => PropertyType.DictionaryProperty; + + private Dictionary entries; + + public object Value + { + get => entries; + set => entries = (Dictionary)value; + } + + public void Read(BinaryReader br) + { + long curPos = br.BaseStream.Position; + + uint numEntries = br.ReadUInt32(); + + for (uint i = 0; i < numEntries; i++) + { + DictionaryEntry de = new DictionaryEntry(codePage); + + de.Read(br); + entries.Add(de.PropertyIdentifier, de.Name); + } + + int m = (int)(br.BaseStream.Position - curPos) % 4; + + if (m > 0) + { + for (int i = 0; i < m; i++) + { + br.ReadByte(); + } + } + } + + /// + /// Write the dictionary and all its values into the specified . + /// + /// + /// Based on the Microsoft specifications at https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/99127b7f-c440-4697-91a4-c853086d6b33 + /// + /// A writer to write the dictionary into. + public void Write(BinaryWriter bw) + { + long curPos = bw.BaseStream.Position; + + bw.Write(entries.Count); + + foreach (KeyValuePair kv in entries) + { + WriteEntry(bw, kv.Key, kv.Value); + } + + int size = (int)(bw.BaseStream.Position - curPos); + WritePaddingIfNeeded(bw, size); + } + + // Write a single entry to the dictionary, and handle and required null termination and padding. + private void WriteEntry(BinaryWriter bw, uint propertyIdentifier, string name) + { + // Write the PropertyIdentifier + bw.Write(propertyIdentifier); + + // Encode string data with the current codepage + byte[] nameBytes = Encoding.GetEncoding(codePage).GetBytes(name); + uint byteLength = (uint)nameBytes.Length; + + // If the code page is WINUNICODE, write the length as the number of characters and pad the length to a multiple of 4 bytes + // Otherwise, write the length as the number of bytes and don't pad. + // In either case, the string must be null terminated + if (codePage == CodePages.WinUnicode) + { + bool addNullTerminator = + byteLength == 0 || nameBytes[byteLength - 1] != '\0' || nameBytes[byteLength - 2] != '\0'; + + if (addNullTerminator) + byteLength += 2; + + bw.Write(byteLength / 2); + bw.Write(nameBytes); + + if (addNullTerminator) + { + bw.Write((byte)0); + bw.Write((byte)0); + } + + WritePaddingIfNeeded(bw, (int)byteLength); + } + else + { + bool addNullTerminator = + byteLength == 0 || nameBytes[byteLength - 1] != '\0'; + + if (addNullTerminator) + byteLength += 1; + + bw.Write(byteLength); + bw.Write(nameBytes); + + if (addNullTerminator) + bw.Write((byte)0); + } + } + + // Write as much padding as needed to pad fieldLength to a multiple of 4 bytes + private void WritePaddingIfNeeded(BinaryWriter bw, int fieldLength) + { + int m = fieldLength % 4; + + if (m > 0) + { + for (int i = 0; i < 4 - m; i++) // padding + bw.Write((byte)0); + } + } +} + diff --git a/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs new file mode 100644 index 00000000..18602bd8 --- /dev/null +++ b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs @@ -0,0 +1,16 @@ +namespace OpenMcdf.Ole; + +// A separate factory for DocumentSummaryInformation properties, to handle special cases with unaligned strings. +internal sealed class DocumentSummaryInfoPropertyFactory : PropertyFactory +{ + public static PropertyFactory Instance { get; } = new DocumentSummaryInfoPropertyFactory(); + + protected override ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) + { + // PIDDSI_HEADINGPAIR and PIDDSI_DOCPARTS use unaligned (unpadded) strings - the others are padded + if (propertyIdentifier == 0x0000000C || propertyIdentifier == 0x0000000D) + return new VT_Unaligned_LPSTR_Property(vType, codePage, isVariant); + + return base.CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); + } +} diff --git a/OpenMcdf.Ole/IBinarySerializable.cs b/OpenMcdf.Ole/IBinarySerializable.cs new file mode 100644 index 00000000..b04e8a7c --- /dev/null +++ b/OpenMcdf.Ole/IBinarySerializable.cs @@ -0,0 +1,7 @@ +namespace OpenMcdf.Ole; + +public interface IBinarySerializable +{ + void Write(BinaryWriter bw); + void Read(BinaryReader br); +} diff --git a/OpenMcdf.Ole/IDictionaryProperty.cs b/OpenMcdf.Ole/IDictionaryProperty.cs new file mode 100644 index 00000000..26246505 --- /dev/null +++ b/OpenMcdf.Ole/IDictionaryProperty.cs @@ -0,0 +1,6 @@ +namespace OpenMcdf.Ole +{ + public interface IDictionaryProperty : IProperty + { + } +} \ No newline at end of file diff --git a/OpenMcdf.Ole/IProperty.cs b/OpenMcdf.Ole/IProperty.cs new file mode 100644 index 00000000..9d44eed4 --- /dev/null +++ b/OpenMcdf.Ole/IProperty.cs @@ -0,0 +1,21 @@ +namespace OpenMcdf.Ole; + +public enum PropertyType +{ + TypedPropertyValue = 0, + DictionaryProperty = 1 +} + +public interface IProperty : IBinarySerializable +{ + object Value + { + get; + set; + } + + PropertyType PropertyType + { + get; + } +} diff --git a/OpenMcdf.Ole/ITypedPropertyValue.cs b/OpenMcdf.Ole/ITypedPropertyValue.cs new file mode 100644 index 00000000..9c316265 --- /dev/null +++ b/OpenMcdf.Ole/ITypedPropertyValue.cs @@ -0,0 +1,21 @@ +namespace OpenMcdf.Ole +{ + public interface ITypedPropertyValue : IProperty + { + VTPropertyType VTType + { + get; + //set; + } + + PropertyDimensions PropertyDimensions + { + get; + } + + bool IsVariant + { + get; + } + } +} diff --git a/OpenMcdf.Ole/Identifiers.cs b/OpenMcdf.Ole/Identifiers.cs new file mode 100644 index 00000000..860297d0 --- /dev/null +++ b/OpenMcdf.Ole/Identifiers.cs @@ -0,0 +1,27 @@ +namespace OpenMcdf.Ole; + +public static class Identifiers +{ + public static string GetDescription(uint identifier, ContainerType map, IDictionary customDict = null) + { + IDictionary nameDictionary = customDict; + + if (nameDictionary is null) + { + switch (map) + { + case ContainerType.SummaryInfo: + nameDictionary = PropertyIdentifiers.SummaryInfo; + break; + case ContainerType.DocumentSummaryInfo: + nameDictionary = PropertyIdentifiers.DocumentSummaryInfo; + break; + } + } + + if (nameDictionary?.TryGetValue(identifier, out string? value) == true) + return value; + + return $"0x{identifier:x8}"; + } +} diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs new file mode 100644 index 00000000..073afbdf --- /dev/null +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -0,0 +1,316 @@ +namespace OpenMcdf.Ole; + +public enum ContainerType +{ + AppSpecific = 0, + SummaryInfo = 1, + DocumentSummaryInfo = 2, + UserDefinedProperties = 3, + GlobalInfo = 4, + ImageContents = 5, + ImageInfo = 6 +} + +public class OlePropertiesContainer +{ + public Dictionary PropertyNames; + + public OlePropertiesContainer UserDefinedProperties { get; private set; } + + public bool HasUserDefinedProperties { get; private set; } + + public ContainerType ContainerType { get; } + private Guid? FmtID0 { get; } + + public PropertyContext Context { get; } + + private readonly List properties = new(); + internal Stream cfStream; + + /* + Property name Property ID PID Type + Codepage PID_CODEPAGE 1 VT_I2 + Title PID_TITLE 2 VT_LPSTR + Subject PID_SUBJECT 3 VT_LPSTR + Author PID_AUTHOR 4 VT_LPSTR + Keywords PID_KEYWORDS 5 VT_LPSTR + Comments PID_COMMENTS 6 VT_LPSTR + Template PID_TEMPLATE 7 VT_LPSTR + Last Saved By PID_LASTAUTHOR 8 VT_LPSTR + Revision Number PID_REVNUMBER 9 VT_LPSTR + Last Printed PID_LASTPRINTED 11 VT_FILETIME + Create Time/Date PID_CREATE_DTM 12 VT_FILETIME + Last Save Time/Date PID_LASTSAVE_DTM 13 VT_FILETIME + Page Count PID_PAGECOUNT 14 VT_I4 + Word Count PID_WORDCOUNT 15 VT_I4 + Character Count PID_CHARCOUNT 16 VT_I4 + Creating Application PID_APPNAME 18 VT_LPSTR + Security PID_SECURITY 19 VT_I4 + */ + public class SummaryInfoProperties + { + public short CodePage { get; set; } + public string Title { get; set; } + public string Subject { get; set; } + public string Author { get; set; } + public string KeyWords { get; set; } + public string Comments { get; set; } + public string Template { get; set; } + public string LastSavedBy { get; set; } + public string RevisionNumber { get; set; } + public DateTime LastPrinted { get; set; } + public DateTime CreateTime { get; set; } + public DateTime LastSavedTime { get; set; } + public int PageCount { get; set; } + public int WordCount { get; set; } + public int CharacterCount { get; set; } + public string CreatingApplication { get; set; } + public int Security { get; set; } + } + + public static OlePropertiesContainer CreateNewSummaryInfo(SummaryInfoProperties sumInfoProps) + { + return null; + } + + public OlePropertiesContainer(int codePage, ContainerType containerType) + { + Context = new PropertyContext + { + CodePage = codePage, + Behavior = Behavior.CaseInsensitive + }; + + ContainerType = containerType; + } + + internal OlePropertiesContainer(Stream cfStream) + { + PropertySetStream pStream = new(); + + this.cfStream = cfStream; + + using BinaryReader reader = new(cfStream); + pStream.Read(reader); + + if (pStream.FMTID0 == WellKnownFormatIdentifiers.SummaryInformation) + ContainerType = ContainerType.SummaryInfo; + else if (pStream.FMTID0 == WellKnownFormatIdentifiers.DocSummaryInformation) + ContainerType = ContainerType.DocumentSummaryInfo; + else + ContainerType = ContainerType.AppSpecific; + FmtID0 = pStream.FMTID0; + + PropertyNames = (Dictionary?)pStream.PropertySet0.Properties + .FirstOrDefault(p => p.PropertyType == PropertyType.DictionaryProperty)?.Value; + + Context = new PropertyContext() + { + CodePage = pStream.PropertySet0.PropertyContext.CodePage + }; + + for (int i = 0; i < pStream.PropertySet0.Properties.Count; i++) + { + if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 0) continue; + //if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 1) continue; + //if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 0x80000000) continue; + + var p = (ITypedPropertyValue)pStream.PropertySet0.Properties[i]; + PropertyIdentifierAndOffset poi = pStream.PropertySet0.PropertyIdentifierAndOffsets[i]; + + var op = new OleProperty(this) + { + VTType = p.VTType, + PropertyIdentifier = pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, + Value = p.Value + }; + + properties.Add(op); + } + + if (pStream.NumPropertySets == 2) + { + UserDefinedProperties = new OlePropertiesContainer(pStream.PropertySet1.PropertyContext.CodePage, ContainerType.UserDefinedProperties); + HasUserDefinedProperties = true; + + for (int i = 0; i < pStream.PropertySet1.Properties.Count; i++) + { + if (pStream.PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier is 0 or 0x80000000) + continue; + + var p = (ITypedPropertyValue)pStream.PropertySet1.Properties[i]; + PropertyIdentifierAndOffset poi = pStream.PropertySet1.PropertyIdentifierAndOffsets[i]; + + OleProperty op = new(UserDefinedProperties) + { + VTType = p.VTType, + PropertyIdentifier = pStream.PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, + Value = p.Value + }; + + UserDefinedProperties.properties.Add(op); + } + + var existingPropertyNames = (Dictionary?)pStream.PropertySet1.Properties + .FirstOrDefault(p => p.PropertyType == PropertyType.DictionaryProperty)?.Value; + + UserDefinedProperties.PropertyNames = existingPropertyNames ?? new Dictionary(); + } + } + + public IList Properties => properties; + + public OleProperty NewProperty(VTPropertyType vtPropertyType, uint propertyIdentifier, string? propertyName = null) + { + //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); + var op = new OleProperty(this) + { + VTType = vtPropertyType, + PropertyIdentifier = propertyIdentifier + }; + + return op; + } + + public void AddProperty(OleProperty property) + { + //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); + properties.Add(property); + } + + public void RemoveProperty(uint propertyIdentifier) + { + //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); + OleProperty? toRemove = properties.FirstOrDefault(o => o.PropertyIdentifier == propertyIdentifier); + + if (toRemove != null) + properties.Remove(toRemove); + } + + /// + /// Create a new UserDefinedProperties container within this container. + /// + /// + /// Only containers of type DocumentSummaryInfo can contain user defined properties. + /// + /// The code page to use for the user defined properties. + /// The UserDefinedProperties container. + /// If this container is a type that doesn't suppose user defined properties. + public OlePropertiesContainer CreateUserDefinedProperties(int codePage) + { + // Only the DocumentSummaryInfo stream can contain a UserDefinedProperties + if (ContainerType != ContainerType.DocumentSummaryInfo) + throw new InvalidOperationException($"Only a DocumentSummaryInfo can contain user defined properties. Current container type is {ContainerType}"); + + // Create the container, and add the codepage to the initial set of properties + UserDefinedProperties = new OlePropertiesContainer(codePage, ContainerType.UserDefinedProperties) + { + PropertyNames = new Dictionary() + }; + + var op = new OleProperty(UserDefinedProperties) + { + VTType = VTPropertyType.VT_I2, + PropertyIdentifier = 1, + Value = (short)codePage + }; + + UserDefinedProperties.properties.Add(op); + HasUserDefinedProperties = true; + + return UserDefinedProperties; + } + + public void Save(Stream cfStream) + { + //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); + //properties.Sort((a, b) => a.PropertyIdentifier.CompareTo(b.PropertyIdentifier)); + + using BinaryWriter bw = new(cfStream); + + Guid fmtId0 = FmtID0 ?? (ContainerType == ContainerType.SummaryInfo ? WellKnownFormatIdentifiers.SummaryInformation : WellKnownFormatIdentifiers.DocSummaryInformation); + + PropertySetStream ps = new() + { + ByteOrder = 0xFFFE, + Version = 0, + SystemIdentifier = 0x00020006, + CLSID = Guid.Empty, + + NumPropertySets = 1, + + FMTID0 = fmtId0, + Offset0 = 0, + + FMTID1 = Guid.Empty, + Offset1 = 0, + + PropertySet0 = new PropertySet + { + NumProperties = (uint)Properties.Count, + PropertyIdentifierAndOffsets = new List(), + Properties = new List(), + PropertyContext = Context + } + }; + + // If we're writing an AppSpecific property set and have property names, then add a dictionary property + if (ContainerType == ContainerType.AppSpecific && PropertyNames != null && PropertyNames.Count > 0) + { + AddDictionaryPropertyToPropertySet(PropertyNames, ps.PropertySet0); + ps.PropertySet0.NumProperties += 1; + } + + PropertyFactory factory = + ContainerType == ContainerType.DocumentSummaryInfo ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; + + foreach (OleProperty op in Properties) + { + ITypedPropertyValue p = factory.NewProperty(op.VTType, Context.CodePage, op.PropertyIdentifier); + p.Value = op.Value; + ps.PropertySet0.Properties.Add(p); + ps.PropertySet0.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); + } + + if (HasUserDefinedProperties) + { + ps.NumPropertySets = 2; + + ps.PropertySet1 = new PropertySet + { + // Number of user defined properties, plus 1 for the name dictionary + NumProperties = (uint)UserDefinedProperties.Properties.Count + 1, + PropertyIdentifierAndOffsets = new List(), + Properties = new List(), + PropertyContext = UserDefinedProperties.Context + }; + + ps.FMTID1 = WellKnownFormatIdentifiers.UserDefinedProperties; + ps.Offset1 = 0; + + // Add the dictionary containing the property names + AddDictionaryPropertyToPropertySet(UserDefinedProperties.PropertyNames, ps.PropertySet1); + + // Add the properties themselves + foreach (OleProperty op in UserDefinedProperties.Properties) + { + ITypedPropertyValue p = DefaultPropertyFactory.Instance.NewProperty(op.VTType, ps.PropertySet1.PropertyContext.CodePage, op.PropertyIdentifier); + p.Value = op.Value; + ps.PropertySet1.Properties.Add(p); + ps.PropertySet1.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); + } + } + + ps.Write(bw); + } + + private void AddDictionaryPropertyToPropertySet(Dictionary propertyNames, PropertySet propertySet) + { + IDictionaryProperty dictionaryProperty = new DictionaryProperty(propertySet.PropertyContext.CodePage) + { + Value = propertyNames + }; + propertySet.Properties.Add(dictionaryProperty); + propertySet.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = 0, Offset = 0 }); + } +} diff --git a/OpenMcdf.Ole/OleProperty.cs b/OpenMcdf.Ole/OleProperty.cs new file mode 100644 index 00000000..6d8cefbf --- /dev/null +++ b/OpenMcdf.Ole/OleProperty.cs @@ -0,0 +1,64 @@ +namespace OpenMcdf.Ole; + +public class OleProperty +{ + private readonly OlePropertiesContainer container; + + internal OleProperty(OlePropertiesContainer container) + { + this.container = container; + } + + public string PropertyName => DecodePropertyIdentifier(); + + private string DecodePropertyIdentifier() + { + return Identifiers.GetDescription(PropertyIdentifier, container.ContainerType, container.PropertyNames); + } + + //public string Description { get { return description; } + public uint PropertyIdentifier { get; internal set; } + + public VTPropertyType VTType + { + get; + internal set; + } + + object value; + + public object Value + { + get + { + switch (VTType) + { + case VTPropertyType.VT_LPSTR: + case VTPropertyType.VT_LPWSTR: + if (value is string str && !string.IsNullOrEmpty(str)) + return str.Trim('\0'); + break; + default: + return value; + } + + return value; + } + set => this.value = value; + } + + public override bool Equals(object obj) + { + if (obj is not OleProperty other) + return false; + + return other.PropertyIdentifier == PropertyIdentifier; + } + + public override int GetHashCode() + { + return (int)PropertyIdentifier; + } + + public override string ToString() => $"{PropertyName} - {VTType} - {Value}"; +} diff --git a/OpenMcdf.Ole/OpenMcdf.Ole.csproj b/OpenMcdf.Ole/OpenMcdf.Ole.csproj new file mode 100644 index 00000000..101f0831 --- /dev/null +++ b/OpenMcdf.Ole/OpenMcdf.Ole.csproj @@ -0,0 +1,16 @@ + + + netstandard2.0;net8.0 + 12.0 + enable + enable + + + + + + + + + + \ No newline at end of file diff --git a/OpenMcdf.Ole/PropertyContext.cs b/OpenMcdf.Ole/PropertyContext.cs new file mode 100644 index 00000000..abbe0a2d --- /dev/null +++ b/OpenMcdf.Ole/PropertyContext.cs @@ -0,0 +1,14 @@ +namespace OpenMcdf.Ole; + +public enum Behavior +{ + CaseSensitive, + CaseInsensitive +} + +public class PropertyContext +{ + public int CodePage { get; set; } + public Behavior Behavior { get; set; } + public uint Locale { get; set; } +} diff --git a/OpenMcdf.Ole/PropertyFactory.cs b/OpenMcdf.Ole/PropertyFactory.cs new file mode 100644 index 00000000..c2c201ac --- /dev/null +++ b/OpenMcdf.Ole/PropertyFactory.cs @@ -0,0 +1,665 @@ +using System.Text; + +namespace OpenMcdf.Ole; + +internal abstract class PropertyFactory +{ + static PropertyFactory() + { +#if NETSTANDARD2_0_OR_GREATER + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + } + + public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant = false) + { + ITypedPropertyValue pr = (VTPropertyType)((ushort)vType & 0x00FF) switch + { + VTPropertyType.VT_I1 => new VT_I1_Property(vType, isVariant), + VTPropertyType.VT_I2 => new VT_I2_Property(vType, isVariant), + VTPropertyType.VT_I4 => new VT_I4_Property(vType, isVariant), + VTPropertyType.VT_R4 => new VT_R4_Property(vType, isVariant), + VTPropertyType.VT_R8 => new VT_R8_Property(vType, isVariant), + VTPropertyType.VT_CY => new VT_CY_Property(vType, isVariant), + VTPropertyType.VT_DATE => new VT_DATE_Property(vType, isVariant), + VTPropertyType.VT_INT => new VT_INT_Property(vType, isVariant), + VTPropertyType.VT_UINT => new VT_UINT_Property(vType, isVariant), + VTPropertyType.VT_UI1 => new VT_UI1_Property(vType, isVariant), + VTPropertyType.VT_UI2 => new VT_UI2_Property(vType, isVariant), + VTPropertyType.VT_UI4 => new VT_UI4_Property(vType, isVariant), + VTPropertyType.VT_UI8 => new VT_UI8_Property(vType, isVariant), + VTPropertyType.VT_BSTR => new VT_LPSTR_Property(vType, codePage, isVariant), + VTPropertyType.VT_LPSTR => CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant), + VTPropertyType.VT_LPWSTR => new VT_LPWSTR_Property(vType, codePage, isVariant), + VTPropertyType.VT_FILETIME => new VT_FILETIME_Property(vType, isVariant), + VTPropertyType.VT_DECIMAL => new VT_DECIMAL_Property(vType, isVariant), + VTPropertyType.VT_BOOL => new VT_BOOL_Property(vType, isVariant), + VTPropertyType.VT_EMPTY => new VT_EMPTY_Property(vType, isVariant), + VTPropertyType.VT_VARIANT_VECTOR => new VT_VariantVector(vType, codePage, isVariant, this, propertyIdentifier), + VTPropertyType.VT_CF => new VT_CF_Property(vType, isVariant), + VTPropertyType.VT_BLOB_OBJECT or VTPropertyType.VT_BLOB => new VT_BLOB_Property(vType, isVariant), + VTPropertyType.VT_CLSID => new VT_CLSID_Property(vType, isVariant), + _ => throw new Exception("Unrecognized property type"), + }; + return pr; + } + + protected virtual ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) + { + return new VT_LPSTR_Property(vType, codePage, isVariant); + } + + #region Property implementations + + private class VT_EMPTY_Property : TypedPropertyValue + { + public VT_EMPTY_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override object ReadScalarValue(BinaryReader br) + { + return null; + } + + public override void WriteScalarValue(BinaryWriter bw, object pValue) + { + } + } + + private class VT_I1_Property : TypedPropertyValue + { + public VT_I1_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override sbyte ReadScalarValue(BinaryReader br) + { + sbyte r = br.ReadSByte(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, sbyte pValue) + { + bw.Write(pValue); + } + } + + private class VT_UI1_Property : TypedPropertyValue + { + public VT_UI1_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override byte ReadScalarValue(BinaryReader br) + { + byte r = br.ReadByte(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, byte pValue) + { + bw.Write(pValue); + } + } + + private class VT_UI4_Property : TypedPropertyValue + { + public VT_UI4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override uint ReadScalarValue(BinaryReader br) + { + uint r = br.ReadUInt32(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, uint pValue) + { + bw.Write(pValue); + } + } + + private class VT_UI8_Property : TypedPropertyValue + { + public VT_UI8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override ulong ReadScalarValue(BinaryReader br) + { + ulong r = br.ReadUInt64(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, ulong pValue) + { + bw.Write(pValue); + } + } + + private class VT_I2_Property : TypedPropertyValue + { + public VT_I2_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override short ReadScalarValue(BinaryReader br) + { + short r = br.ReadInt16(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, short pValue) + { + bw.Write(pValue); + } + } + + private class VT_UI2_Property : TypedPropertyValue + { + public VT_UI2_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override ushort ReadScalarValue(BinaryReader br) + { + ushort r = br.ReadUInt16(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, ushort pValue) + { + bw.Write(pValue); + } + } + + private class VT_I4_Property : TypedPropertyValue + { + public VT_I4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override int ReadScalarValue(BinaryReader br) + { + int r = br.ReadInt32(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, int pValue) + { + bw.Write(pValue); + } + } + + private class VT_I8_Property : TypedPropertyValue + { + public VT_I8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override long ReadScalarValue(BinaryReader br) + { + long r = br.ReadInt64(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, long pValue) + { + bw.Write(pValue); + } + } + + private class VT_INT_Property : TypedPropertyValue + { + public VT_INT_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override int ReadScalarValue(BinaryReader br) + { + int r = br.ReadInt32(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, int pValue) + { + bw.Write(pValue); + } + } + + private class VT_UINT_Property : TypedPropertyValue + { + public VT_UINT_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override uint ReadScalarValue(BinaryReader br) + { + uint r = br.ReadUInt32(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, uint pValue) + { + bw.Write(pValue); + } + } + + private class VT_R4_Property : TypedPropertyValue + { + public VT_R4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override float ReadScalarValue(BinaryReader br) + { + float r = br.ReadSingle(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, float pValue) + { + bw.Write(pValue); + } + } + + private class VT_R8_Property : TypedPropertyValue + { + public VT_R8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override double ReadScalarValue(BinaryReader br) + { + double r = br.ReadDouble(); + return r; + } + + public override void WriteScalarValue(BinaryWriter bw, double pValue) + { + bw.Write(pValue); + } + } + + private class VT_CY_Property : TypedPropertyValue + { + public VT_CY_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override long ReadScalarValue(BinaryReader br) + { + long temp = br.ReadInt64(); + + long tmp = temp /= 10000; + + return tmp; + } + + public override void WriteScalarValue(BinaryWriter bw, long pValue) + { + bw.Write(pValue * 10000); + } + } + + private class VT_DATE_Property : TypedPropertyValue + { + public VT_DATE_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override DateTime ReadScalarValue(BinaryReader br) + { + double temp = br.ReadDouble(); + + return DateTime.FromOADate(temp); + } + + public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) + { + bw.Write(pValue.ToOADate()); + } + } + + protected class VT_LPSTR_Property : TypedPropertyValue + { + private byte[] data; + private readonly int codePage; + + public VT_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, isVariant) + { + this.codePage = codePage; + } + + public override string ReadScalarValue(BinaryReader br) + { + uint size = br.ReadUInt32(); + data = br.ReadBytes((int)size); + + string result = Encoding.GetEncoding(codePage).GetString(data); + //result = result.Trim(new char[] { '\0' }); + + //if (this.codePage == CodePages.CP_WINUNICODE) + //{ + // result = result.Substring(0, result.Length - 2); + //} + //else + //{ + // result = result.Substring(0, result.Length - 1); + //} + + return result; + } + + public override void WriteScalarValue(BinaryWriter bw, string pValue) + { + //bool addNullTerminator = true; + + if (string.IsNullOrEmpty(pValue)) //|| String.IsNullOrEmpty(pValue.Trim(new char[] { '\0' }))) + { + bw.Write((uint)0); + } + else if (codePage == CodePages.WinUnicode) + { + data = Encoding.GetEncoding(codePage).GetBytes(pValue); + + //if (data.Length >= 2 && data[data.Length - 2] == '\0' && data[data.Length - 1] == '\0') + // addNullTerminator = false; + + uint dataLength = (uint)data.Length; + + //if (addNullTerminator) + dataLength += 2; // null terminator \u+0000 + + // var mod = dataLength % 4; // pad to multiple of 4 bytes + + bw.Write(dataLength); // datalength of string + null char (unicode) + bw.Write(data); // string + + //if (addNullTerminator) + //{ + bw.Write('\0'); // first byte of null unicode char + bw.Write('\0'); // second byte of null unicode char + //} + + //for (int i = 0; i < (4 - mod); i++) // padding + // bw.Write('\0'); + } + else + { + data = Encoding.GetEncoding(codePage).GetBytes(pValue); + + //if (data.Length >= 1 && data[data.Length - 1] == '\0') + // addNullTerminator = false; + + uint dataLength = (uint)data.Length; + + //if (addNullTerminator) + dataLength += 1; // null terminator \u+0000 + + uint mod = dataLength % 4; // pad to multiple of 4 bytes + + bw.Write(dataLength); // datalength of string + null char (unicode) + bw.Write(data); // string + + //if (addNullTerminator) + //{ + bw.Write('\0'); // null terminator'\0' + //} + + //for (int i = 0; i < (4 - mod); i++) // padding + // bw.Write('\0'); + } + } + } + + protected class VT_Unaligned_LPSTR_Property : VT_LPSTR_Property + { + public VT_Unaligned_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, codePage, isVariant) + { + NeedsPadding = false; + } + } + + private class VT_LPWSTR_Property : TypedPropertyValue + { + private byte[] data; + private readonly int codePage; + + public VT_LPWSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, isVariant) + { + this.codePage = codePage; + } + + public override string ReadScalarValue(BinaryReader br) + { + uint nChars = br.ReadUInt32(); + data = br.ReadBytes((int)((nChars - 1) * 2)); //WChar- null terminator + br.ReadBytes(2); // Skip null terminator + string result = Encoding.Unicode.GetString(data); + //result = result.Trim(new char[] { '\0' }); + + return result; + } + + public override void WriteScalarValue(BinaryWriter bw, string pValue) + { + data = Encoding.Unicode.GetBytes(pValue); + + // The written data length field is the number of characters (not bytes) and must include a null terminator + // add a null terminator if there isn't one already + int byteLength = data.Length; + + //bool addNullTerminator = + // byteLength == 0 || data[byteLength - 1] != '\0' || data[byteLength - 2] != '\0'; + + //if (addNullTerminator) + byteLength += 2; + + bw.Write((uint)byteLength / 2); + bw.Write(data); + + //if (addNullTerminator) + //{ + bw.Write((byte)0); + bw.Write((byte)0); + //} + + //var mod = byteLength % 4; // pad to multiple of 4 bytes + //for (int i = 0; i < (4 - mod); i++) // padding + // bw.Write('\0'); + } + } + + private class VT_FILETIME_Property : TypedPropertyValue + { + public VT_FILETIME_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override DateTime ReadScalarValue(BinaryReader br) + { + long tmp = br.ReadInt64(); + + return DateTime.FromFileTimeUtc(tmp); + } + + public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) + { + bw.Write(pValue.ToFileTimeUtc()); + } + } + + private class VT_DECIMAL_Property : TypedPropertyValue + { + public VT_DECIMAL_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override decimal ReadScalarValue(BinaryReader br) + { + decimal d; + + br.ReadInt16(); // wReserved + byte scale = br.ReadByte(); + byte sign = br.ReadByte(); + + uint u = br.ReadUInt32(); + d = Convert.ToDecimal(Math.Pow(2, 64)) * u; + d += br.ReadUInt64(); + + if (sign != 0) + d = -d; + d /= 10 << scale; + + propertyValue = d; + return d; + } + + public override void WriteScalarValue(BinaryWriter bw, decimal pValue) + { + int[] parts = decimal.GetBits(pValue); + + bool sign = (parts[3] & 0x80000000) != 0; + byte scale = (byte)((parts[3] >> 16) & 0x7F); + + bw.Write((short)0); + bw.Write(scale); + bw.Write(sign ? (byte)0 : (byte)1); + + bw.Write(parts[2]); + bw.Write(parts[1]); + bw.Write(parts[0]); + } + } + + private class VT_BOOL_Property : TypedPropertyValue + { + public VT_BOOL_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override bool ReadScalarValue(BinaryReader br) + { + propertyValue = br.ReadUInt16() == 0xFFFF ? true : false; + return (bool)propertyValue; + //br.ReadUInt16();//padding + } + + public override void WriteScalarValue(BinaryWriter bw, bool pValue) + { + bw.Write(pValue ? (ushort)0xFFFF : (ushort)0); + } + } + + private class VT_CF_Property : TypedPropertyValue + { + public VT_CF_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override object ReadScalarValue(BinaryReader br) + { + uint size = br.ReadUInt32(); + byte[] data = br.ReadBytes((int)size); + return data; + //br.ReadUInt16();//padding + } + + public override void WriteScalarValue(BinaryWriter bw, object pValue) + { + if (pValue is not byte[] r) + { + bw.Write(0u); + } + else + { + bw.Write((uint)r.Length); + bw.Write(r); + } + } + } + + private class VT_BLOB_Property : TypedPropertyValue + { + public VT_BLOB_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override object ReadScalarValue(BinaryReader br) + { + uint size = br.ReadUInt32(); + byte[] data = br.ReadBytes((int)size); + return data; + } + + public override void WriteScalarValue(BinaryWriter bw, object pValue) + { + if (pValue is not byte[] r) + { + bw.Write(0u); + } + else + { + bw.Write((uint)r.Length); + bw.Write(r); + } + } + } + + private class VT_CLSID_Property : TypedPropertyValue + { + public VT_CLSID_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) + { + } + + public override object ReadScalarValue(BinaryReader br) + { + byte[] data = br.ReadBytes(16); + return new Guid(data); + } + + public override void WriteScalarValue(BinaryWriter bw, object pValue) + { + if (pValue is byte[] r) + bw.Write(r); + } + } + + private class VT_VariantVector : TypedPropertyValue + { + private readonly int codePage; + private readonly PropertyFactory factory; + private readonly uint propertyIdentifier; + + public VT_VariantVector(VTPropertyType vType, int codePage, bool isVariant, PropertyFactory factory, uint propertyIdentifier) : base(vType, isVariant) + { + this.codePage = codePage; + this.factory = factory; + this.propertyIdentifier = propertyIdentifier; + NeedsPadding = false; + } + + public override object ReadScalarValue(BinaryReader br) + { + VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); + br.ReadUInt16(); // Ushort Padding + + ITypedPropertyValue p = factory.NewProperty(vType, codePage, propertyIdentifier, true); + p.Read(br); + return p; + } + + public override void WriteScalarValue(BinaryWriter bw, object pValue) + { + ITypedPropertyValue p = (ITypedPropertyValue)pValue; + + p.Write(bw); + } + } + + #endregion + +} diff --git a/OpenMcdf.Ole/PropertyIdentifierAndOffset.cs b/OpenMcdf.Ole/PropertyIdentifierAndOffset.cs new file mode 100644 index 00000000..0e2e0c88 --- /dev/null +++ b/OpenMcdf.Ole/PropertyIdentifierAndOffset.cs @@ -0,0 +1,19 @@ +namespace OpenMcdf.Ole; + +public class PropertyIdentifierAndOffset : IBinarySerializable +{ + public uint PropertyIdentifier { get; set; } + public uint Offset { get; set; } + + public void Read(BinaryReader br) + { + PropertyIdentifier = br.ReadUInt32(); + Offset = br.ReadUInt32(); + } + + public void Write(BinaryWriter bw) + { + bw.Write(PropertyIdentifier); + bw.Write(Offset); + } +} diff --git a/OpenMcdf.Ole/PropertySet.cs b/OpenMcdf.Ole/PropertySet.cs new file mode 100644 index 00000000..dcc8f990 --- /dev/null +++ b/OpenMcdf.Ole/PropertySet.cs @@ -0,0 +1,32 @@ +namespace OpenMcdf.Ole; + +internal sealed class PropertySet +{ + public PropertyContext PropertyContext + { + get; set; + } + + public uint Size { get; set; } + + public uint NumProperties { get; set; } + + public List PropertyIdentifierAndOffsets { get; set; } = new List(); + + public List Properties { get; set; } = new List(); + + public void LoadContext(int propertySetOffset, BinaryReader br) + { + long currPos = br.BaseStream.Position; + + PropertyContext = new PropertyContext(); + int codePageOffset = (int)(propertySetOffset + PropertyIdentifierAndOffsets.Where(pio => pio.PropertyIdentifier == 1).First().Offset); + br.BaseStream.Seek(codePageOffset, SeekOrigin.Begin); + + VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); + br.ReadUInt16(); // Ushort Padding + PropertyContext.CodePage = (ushort)br.ReadInt16(); + + br.BaseStream.Position = currPos; + } +} diff --git a/OpenMcdf.Ole/PropertySetStream.cs b/OpenMcdf.Ole/PropertySetStream.cs new file mode 100644 index 00000000..3c3acb97 --- /dev/null +++ b/OpenMcdf.Ole/PropertySetStream.cs @@ -0,0 +1,235 @@ +namespace OpenMcdf.Ole; + +internal sealed class PropertySetStream +{ + public ushort ByteOrder { get; set; } + public ushort Version { get; set; } + public uint SystemIdentifier { get; set; } + public Guid CLSID { get; set; } + public uint NumPropertySets { get; set; } + public Guid FMTID0 { get; set; } + public uint Offset0 { get; set; } + public Guid FMTID1 { get; set; } + public uint Offset1 { get; set; } + public PropertySet PropertySet0 { get; set; } + public PropertySet PropertySet1 { get; set; } + + //private SummaryInfoMap map; + + public PropertySetStream() + { + } + + public void Read(BinaryReader br) + { + ByteOrder = br.ReadUInt16(); + Version = br.ReadUInt16(); + SystemIdentifier = br.ReadUInt32(); + CLSID = new Guid(br.ReadBytes(16)); + NumPropertySets = br.ReadUInt32(); + FMTID0 = new Guid(br.ReadBytes(16)); + Offset0 = br.ReadUInt32(); + + if (NumPropertySets == 2) + { + FMTID1 = new Guid(br.ReadBytes(16)); + Offset1 = br.ReadUInt32(); + } + + PropertySet0 = new PropertySet + { + Size = br.ReadUInt32(), + NumProperties = br.ReadUInt32() + }; + + // Create appropriate property factory based on the stream type + PropertyFactory factory = FMTID0 == WellKnownFormatIdentifiers.DocSummaryInformation ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; + + // Read property offsets (P0) + for (int i = 0; i < PropertySet0.NumProperties; i++) + { + PropertyIdentifierAndOffset pio = new() + { + PropertyIdentifier = br.ReadUInt32(), + Offset = br.ReadUInt32() + }; + PropertySet0.PropertyIdentifierAndOffsets.Add(pio); + } + + PropertySet0.LoadContext((int)Offset0, br); //Read CodePage, Locale + + // Read properties (P0) + for (int i = 0; i < PropertySet0.NumProperties; i++) + { + br.BaseStream.Seek(Offset0 + PropertySet0.PropertyIdentifierAndOffsets[i].Offset, SeekOrigin.Begin); + PropertySet0.Properties.Add(ReadProperty(PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet0.PropertyContext.CodePage, br, factory)); + } + + if (NumPropertySets == 2) + { + br.BaseStream.Seek(Offset1, SeekOrigin.Begin); + PropertySet1 = new PropertySet + { + Size = br.ReadUInt32(), + NumProperties = br.ReadUInt32() + }; + + // Read property offsets + for (int i = 0; i < PropertySet1.NumProperties; i++) + { + PropertyIdentifierAndOffset pio = new() + { + PropertyIdentifier = br.ReadUInt32(), + Offset = br.ReadUInt32() + }; + PropertySet1.PropertyIdentifierAndOffsets.Add(pio); + } + + PropertySet1.LoadContext((int)Offset1, br); + + // Read properties + for (int i = 0; i < PropertySet1.NumProperties; i++) + { + br.BaseStream.Seek(Offset1 + PropertySet1.PropertyIdentifierAndOffsets[i].Offset, SeekOrigin.Begin); + PropertySet1.Properties.Add(ReadProperty(PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet1.PropertyContext.CodePage, br, DefaultPropertyFactory.Instance)); + } + } + } + + private class OffsetContainer + { + public int OffsetPS { get; set; } + + public List PropertyIdentifierOffsets { get; set; } + public List PropertyOffsets { get; set; } + + public OffsetContainer() + { + PropertyOffsets = new List(); + PropertyIdentifierOffsets = new List(); + } + } + + public void Write(BinaryWriter bw) + { + var oc0 = new OffsetContainer(); + var oc1 = new OffsetContainer(); + + bw.Write(ByteOrder); + bw.Write(Version); + bw.Write(SystemIdentifier); + bw.Write(CLSID.ToByteArray()); + bw.Write(NumPropertySets); + bw.Write(FMTID0.ToByteArray()); + bw.Write(Offset0); + + if (NumPropertySets == 2) + { + bw.Write(FMTID1.ToByteArray()); + bw.Write(Offset1); + } + + oc0.OffsetPS = (int)bw.BaseStream.Position; + bw.Write(PropertySet0.Size); + bw.Write(PropertySet0.NumProperties); + + // w property offsets + for (int i = 0; i < PropertySet0.NumProperties; i++) + { + oc0.PropertyIdentifierOffsets.Add(bw.BaseStream.Position); //Offset of 4 to Offset value + PropertySet0.PropertyIdentifierAndOffsets[i].Write(bw); + } + + for (int i = 0; i < PropertySet0.NumProperties; i++) + { + oc0.PropertyOffsets.Add(bw.BaseStream.Position); + PropertySet0.Properties[i].Write(bw); + } + + long padding0 = bw.BaseStream.Position % 4; + + if (padding0 > 0) + { + for (int p = 0; p < 4 - padding0; p++) + bw.Write((byte)0); + } + + int size0 = (int)(bw.BaseStream.Position - oc0.OffsetPS); + + if (NumPropertySets == 2) + { + oc1.OffsetPS = (int)bw.BaseStream.Position; + + bw.Write(PropertySet1.Size); + bw.Write(PropertySet1.NumProperties); + + // w property offsets + for (int i = 0; i < PropertySet1.PropertyIdentifierAndOffsets.Count; i++) + { + oc1.PropertyIdentifierOffsets.Add(bw.BaseStream.Position); //Offset of 4 to Offset value + PropertySet1.PropertyIdentifierAndOffsets[i].Write(bw); + } + + for (int i = 0; i < PropertySet1.NumProperties; i++) + { + oc1.PropertyOffsets.Add(bw.BaseStream.Position); + PropertySet1.Properties[i].Write(bw); + } + + int size1 = (int)(bw.BaseStream.Position - oc1.OffsetPS); + + bw.Seek(oc1.OffsetPS, SeekOrigin.Begin); + bw.Write(size1); + } + + bw.Seek(oc0.OffsetPS, SeekOrigin.Begin); + bw.Write(size0); + + int shiftO1 = 2 + 2 + 4 + 16 + 4 + 16; //OFFSET0 + bw.Seek(shiftO1, SeekOrigin.Begin); + bw.Write(oc0.OffsetPS); + + if (NumPropertySets == 2) + { + bw.Seek(shiftO1 + 4 + 16, SeekOrigin.Begin); + bw.Write(oc1.OffsetPS); + } + + //----------- + + for (int i = 0; i < PropertySet0.PropertyIdentifierAndOffsets.Count; i++) + { + bw.Seek((int)oc0.PropertyIdentifierOffsets[i] + 4, SeekOrigin.Begin); //Offset of 4 to Offset value + bw.Write((int)(oc0.PropertyOffsets[i] - oc0.OffsetPS)); + } + + if (PropertySet1 != null) + { + for (int i = 0; i < PropertySet1.PropertyIdentifierAndOffsets.Count; i++) + { + bw.Seek((int)oc1.PropertyIdentifierOffsets[i] + 4, SeekOrigin.Begin); //Offset of 4 to Offset value + bw.Write((int)(oc1.PropertyOffsets[i] - oc1.OffsetPS)); + } + } + } + + private static IProperty ReadProperty(uint propertyIdentifier, int codePage, BinaryReader br, PropertyFactory factory) + { + if (propertyIdentifier != 0) + { + var vType = (VTPropertyType)br.ReadUInt16(); + br.ReadUInt16(); // Ushort Padding + + ITypedPropertyValue pr = factory.NewProperty(vType, codePage, propertyIdentifier); + pr.Read(br); + + return pr; + } + else + { + DictionaryProperty dictionaryProperty = new(codePage); + dictionaryProperty.Read(br); + return dictionaryProperty; + } + } +} diff --git a/OpenMcdf.Ole/ProperyIdentifiers.cs b/OpenMcdf.Ole/ProperyIdentifiers.cs new file mode 100644 index 00000000..6dd53bf3 --- /dev/null +++ b/OpenMcdf.Ole/ProperyIdentifiers.cs @@ -0,0 +1,48 @@ +using System.Collections.Immutable; + +namespace OpenMcdf.Ole; + +public static class PropertyIdentifiers +{ + public static ImmutableDictionary SummaryInfo { get; } = new Dictionary() + { + {0x00000001, "CodePageString" }, + {0x00000002, "PIDSI_TITLE" }, + {0x00000003, "PIDSI_SUBJECT" }, + {0x00000004, "PIDSI_AUTHOR" }, + {0x00000005, "PIDSI_KEYWORDS" }, + {0x00000006, "PIDSI_COMMENTS" }, + {0x00000007, "PIDSI_TEMPLATE" }, + {0x00000008, "PIDSI_LASTAUTHOR" }, + {0x00000009, "PIDSI_REVNUMBER" }, + {0x00000012, "PIDSI_APPNAME" }, + {0x0000000A, "PIDSI_EDITTIME" }, + {0x0000000B, "PIDSI_LASTPRINTED" }, + {0x0000000C, "PIDSI_CREATE_DTM" }, + {0x0000000D, "PIDSI_LASTSAVE_DTM" }, + {0x0000000E, "PIDSI_PAGECOUNT" }, + {0x0000000F, "PIDSI_WORDCOUNT" }, + {0x00000010, "PIDSI_CHARCOUNT" }, + {0x00000013, "PIDSI_DOC_SECURITY" } + }.ToImmutableDictionary(); + + public static ImmutableDictionary DocumentSummaryInfo { get; } = new Dictionary() + { + {0x00000001, "CodePageString" }, + {0x00000002, "PIDDSI_CATEGORY" }, + {0x00000003, "PIDDSI_PRESFORMAT" }, + {0x00000004, "PIDDSI_BYTECOUNT" }, + {0x00000005, "PIDDSI_LINECOUNT" }, + {0x00000006, "PIDDSI_PARCOUNT" }, + {0x00000007, "PIDDSI_SLIDECOUNT" }, + {0x00000008, "PIDDSI_NOTECOUNT" }, + {0x00000009, "PIDDSI_HIDDENCOUNT" }, + {0x0000000A, "PIDDSI_MMCLIPCOUNT" }, + {0x0000000B, "PIDDSI_SCALE" }, + {0x0000000C, "PIDDSI_HEADINGPAIR" }, + {0x0000000D, "PIDDSI_DOCPARTS" }, + {0x0000000E, "PIDDSI_MANAGER" }, + {0x0000000F, "PIDDSI_COMPANY" }, + {0x00000010, "PIDDSI_LINKSDIRTY" } + }.ToImmutableDictionary(); +} diff --git a/OpenMcdf.Ole/TypedPropertyValue.cs b/OpenMcdf.Ole/TypedPropertyValue.cs new file mode 100644 index 00000000..80d0e447 --- /dev/null +++ b/OpenMcdf.Ole/TypedPropertyValue.cs @@ -0,0 +1,155 @@ +namespace OpenMcdf.Ole; + +internal abstract class TypedPropertyValue : ITypedPropertyValue +{ + private readonly VTPropertyType _VTType; + + public PropertyType PropertyType => PropertyType.TypedPropertyValue; + + public VTPropertyType VTType => _VTType; + + protected object propertyValue; + + public TypedPropertyValue(VTPropertyType vtType, bool isVariant = false) + { + _VTType = vtType; + PropertyDimensions = CheckPropertyDimensions(vtType); + IsVariant = isVariant; + } + + public PropertyDimensions PropertyDimensions { get; } = PropertyDimensions.IsScalar; + + public bool IsVariant { get; } + + protected virtual bool NeedsPadding { get; set; } = true; + + private PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType) + { + if ((((ushort)vtType) & 0x1000) != 0) + return PropertyDimensions.IsVector; + if ((((ushort)vtType) & 0x2000) != 0) + return PropertyDimensions.IsArray; + return PropertyDimensions.IsScalar; + } + + public virtual object Value + { + get => propertyValue; + + set => propertyValue = value; + } + + public abstract T ReadScalarValue(BinaryReader br); + + public void Read(BinaryReader br) + { + long currentPos = br.BaseStream.Position; + + switch (PropertyDimensions) + { + case PropertyDimensions.IsScalar: + { + propertyValue = ReadScalarValue(br); + int size = (int)(br.BaseStream.Position - currentPos); + + int m = size % 4; + + if (m > 0 && NeedsPadding) + br.ReadBytes(4 - m); // padding + } + + break; + + case PropertyDimensions.IsVector: + { + uint nItems = br.ReadUInt32(); + + List res = new List(); + + for (int i = 0; i < nItems; i++) + { + T s = ReadScalarValue(br); + + res.Add(s); + + // The padding in a vector can be per-item + int itemSize = (int)(br.BaseStream.Position - currentPos); + + int pad = itemSize % 4; + if (pad > 0 && NeedsPadding) + br.ReadBytes(4 - pad); // padding + } + + propertyValue = res; + int size = (int)(br.BaseStream.Position - currentPos); + + int m = size % 4; + if (m > 0 && NeedsPadding) + br.ReadBytes(4 - m); // padding + } + + break; + default: + break; + } + } + + public abstract void WriteScalarValue(BinaryWriter bw, T pValue); + + public void Write(BinaryWriter bw) + { + long currentPos = bw.BaseStream.Position; + int size; + int m; + switch (PropertyDimensions) + { + case PropertyDimensions.IsScalar: + + bw.Write((ushort)_VTType); + bw.Write((ushort)0); + + WriteScalarValue(bw, (T)propertyValue); + size = (int)(bw.BaseStream.Position - currentPos); + m = size % 4; + + if (m > 0 && NeedsPadding) + { + for (int i = 0; i < 4 - m; i++) // padding + bw.Write((byte)0); + } + + break; + + case PropertyDimensions.IsVector: + + bw.Write((ushort)_VTType); + bw.Write((ushort)0); + bw.Write((uint)((List)propertyValue).Count); + + for (int i = 0; i < ((List)propertyValue).Count; i++) + { + WriteScalarValue(bw, ((List)propertyValue)[i]); + + size = (int)(bw.BaseStream.Position - currentPos); + m = size % 4; + + if (m > 0 && NeedsPadding) + { + for (int q = 0; q < 4 - m; q++) // padding + bw.Write((byte)0); + } + } + + size = (int)(bw.BaseStream.Position - currentPos); + m = size % 4; + + if (m > 0 && NeedsPadding) + { + for (int i = 0; i < 4 - m; i++) // padding + bw.Write((byte)0); + } + + break; + } + } +} diff --git a/OpenMcdf.Ole/VTPropertyType.cs b/OpenMcdf.Ole/VTPropertyType.cs new file mode 100644 index 00000000..f6084dd3 --- /dev/null +++ b/OpenMcdf.Ole/VTPropertyType.cs @@ -0,0 +1,44 @@ +namespace OpenMcdf.Ole; + +/// +/// VARENUM +/// +public enum VTPropertyType : ushort +{ + VT_EMPTY = 0x0000, + VT_NULL = 0x0001, + VT_I2 = 0x0002, + VT_I4 = 0x0003, + VT_R4 = 0x0004, + VT_R8 = 0x0005, + VT_CY = 0x0006, + VT_DATE = 0x0007, + VT_BSTR = 0x0008, + VT_ERROR = 0x000A, + VT_BOOL = 0x000B, + VT_DECIMAL = 0x000E, + VT_I1 = 0x0010, + VT_UI1 = 0x0011, + VT_UI2 = 0x0012, + VT_UI4 = 0x0013, + VT_I8 = 0x0014, // MUST be an 8-byte signed integer. + VT_UI8 = 0x0015, // MUST be an 8-byte unsigned integer. + VT_INT = 0x0016, // MUST be a 4-byte signed integer. + VT_UINT = 0x0017, // MUST be a 4-byte unsigned integer. + VT_LPSTR = 0x001E, // MUST be a CodePageString. + VT_LPWSTR = 0x001F, // MUST be a UnicodeString. + VT_FILETIME = 0x0040, // MUST be a FILETIME (Packet Version). + VT_BLOB = 0x0041, // MUST be a BLOB. + VT_STREAM = 0x0042, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a stream element with this name. + VT_STORAGE = 0x0043, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a storage element with this name. + VT_STREAMED_OBJECT = 0x0044, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a stream element with this name. + VT_STORED_OBJECT = 0x0045, // MUST be an IndirectPropertyName. The storage representing the (non-simple) property set MUST have a storage element with this name. + VT_BLOB_OBJECT = 0x0046, // MUST be a BLOB. + VT_CF = 0x0047, // MUST be a ClipboardData. + VT_CLSID = 0x0048, // MUST be a GUID (Packet Version) + VT_VERSIONED_STREAM = 0x0049, // MUST be a versioned Stream, NOT allowed in simple property + VT_VECTOR_HEADER = 0x1000, //--- NOT NORMATIVE + VT_ARRAY_HEADER = 0x2000, //--- NOT NORMATIVE + VT_VARIANT_VECTOR = 0x000C, //--- NOT NORMATIVE + VT_VARIANT_ARRAY = 0x200C, //--- NOT NORMATIVE +} diff --git a/OpenMcdf.Ole/WellKnownFormatIdentifiers.cs b/OpenMcdf.Ole/WellKnownFormatIdentifiers.cs new file mode 100644 index 00000000..9161fdbe --- /dev/null +++ b/OpenMcdf.Ole/WellKnownFormatIdentifiers.cs @@ -0,0 +1,11 @@ +namespace OpenMcdf.Ole; + +public static class WellKnownFormatIdentifiers +{ + public static readonly Guid SummaryInformation = new("{F29F85E0-4FF9-1068-AB91-08002B27B3D9}"); + public static readonly Guid DocSummaryInformation = new("{D5CDD502-2E9C-101B-9397-08002B2CF9AE}"); + public static readonly Guid UserDefinedProperties = new("{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"); + public static readonly Guid GlobalInfo = new("{56616F00-C154-11CE-8553-00AA00A1F95B}"); + public static readonly Guid ImageContents = new("{56616400-C154-11CE-8553-00AA00A1F95B}"); + public static readonly Guid ImageInfo = new("{56616500-C154-11CE-8553-00AA00A1F95B}"); +} diff --git a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs index c83cde10..eda2ffd3 100644 --- a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs +++ b/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs @@ -2,7 +2,7 @@ namespace OpenMcdf3.Benchmarks; -internal class StructuredStorageBenchmarks +internal static class StructuredStorageBenchmarks { public static void ReadStream(string fileName, byte[] buffer) { diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 7f045797..6ddb516e 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Perf", "OpenMcdf3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorage", "StructuredStorage\StructuredStorage.csproj", "{D7861D73-B42C-403E-9B9E-F921BC70F0D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Ole", "OpenMcdf.Ole\OpenMcdf.Ole.csproj", "{06FFA945-128E-43FA-B541-38987BC1E0D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {D7861D73-B42C-403E-9B9E-F921BC70F0D3}.Release|Any CPU.Build.0 = Release|Any CPU + {06FFA945-128E-43FA-B541-38987BC1E0D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06FFA945-128E-43FA-B541-38987BC1E0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 30bc811480c8350ff39cdfa2346ee4b5b1fd3476 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Mon, 11 Nov 2024 23:33:55 +1300 Subject: [PATCH 096/114] Add StructuredStorageExplorer --- OpenMcdf.Ole/IDictionaryProperty.cs | 7 +- OpenMcdf.Ole/ITypedPropertyValue.cs | 29 +- OpenMcdf.Ole/OlePropertiesContainer.cs | 6 +- .../OpenMcdf3.Benchmarks.csproj | 2 +- OpenMcdf3.Tests/StorageTests.cs | 2 +- OpenMcdf3.sln | 6 + OpenMcdf3/CfbStream.cs | 2 + OpenMcdf3/DirectoryEntry.cs | 11 +- OpenMcdf3/EntryInfo.cs | 14 +- OpenMcdf3/Storage.cs | 10 +- OpenMcdf3/System/Index.cs | 245 +++++---- OpenMcdf3/System/NullableAttributes.cs | 307 ++++++----- OpenMcdf3/System/Range.cs | 189 ++++--- .../MainForm.Designer.cs | 466 +++++++++++++++++ StructuredStorageExplorer/MainForm.cs | 487 ++++++++++++++++++ StructuredStorageExplorer/MainForm.resx | 141 +++++ StructuredStorageExplorer/OpenMcdf.snk | Bin 0 -> 596 bytes .../PreferencesForm.Designer.cs | 106 ++++ StructuredStorageExplorer/PreferencesForm.cs | 31 ++ .../PreferencesForm.resx | 120 +++++ StructuredStorageExplorer/Program.cs | 15 + .../Properties/Resources.Designer.cs | 123 +++++ .../Properties/Resources.resx | 139 +++++ .../Properties/Settings.Designer.cs | 50 ++ .../Properties/Settings.settings | 12 + .../StreamDataProvider.cs | 160 ++++++ .../StructuredStorageExplorer.csproj | 18 + StructuredStorageExplorer/Utils.cs | 46 ++ StructuredStorageExplorer/app.config | 18 + StructuredStorageExplorer/img/disk.png | Bin 0 -> 620 bytes StructuredStorageExplorer/img/door_out.png | Bin 0 -> 688 bytes StructuredStorageExplorer/img/folder.png | Bin 0 -> 537 bytes StructuredStorageExplorer/img/page_white.png | Bin 0 -> 294 bytes StructuredStorageExplorer/img/storage.png | Bin 0 -> 396 bytes StructuredStorageExplorer/img/stream.png | Bin 0 -> 409 bytes 35 files changed, 2356 insertions(+), 406 deletions(-) create mode 100644 StructuredStorageExplorer/MainForm.Designer.cs create mode 100644 StructuredStorageExplorer/MainForm.cs create mode 100644 StructuredStorageExplorer/MainForm.resx create mode 100644 StructuredStorageExplorer/OpenMcdf.snk create mode 100644 StructuredStorageExplorer/PreferencesForm.Designer.cs create mode 100644 StructuredStorageExplorer/PreferencesForm.cs create mode 100644 StructuredStorageExplorer/PreferencesForm.resx create mode 100644 StructuredStorageExplorer/Program.cs create mode 100644 StructuredStorageExplorer/Properties/Resources.Designer.cs create mode 100644 StructuredStorageExplorer/Properties/Resources.resx create mode 100644 StructuredStorageExplorer/Properties/Settings.Designer.cs create mode 100644 StructuredStorageExplorer/Properties/Settings.settings create mode 100644 StructuredStorageExplorer/StreamDataProvider.cs create mode 100644 StructuredStorageExplorer/StructuredStorageExplorer.csproj create mode 100644 StructuredStorageExplorer/Utils.cs create mode 100644 StructuredStorageExplorer/app.config create mode 100644 StructuredStorageExplorer/img/disk.png create mode 100644 StructuredStorageExplorer/img/door_out.png create mode 100644 StructuredStorageExplorer/img/folder.png create mode 100644 StructuredStorageExplorer/img/page_white.png create mode 100644 StructuredStorageExplorer/img/storage.png create mode 100644 StructuredStorageExplorer/img/stream.png diff --git a/OpenMcdf.Ole/IDictionaryProperty.cs b/OpenMcdf.Ole/IDictionaryProperty.cs index 26246505..8b20b053 100644 --- a/OpenMcdf.Ole/IDictionaryProperty.cs +++ b/OpenMcdf.Ole/IDictionaryProperty.cs @@ -1,6 +1,5 @@ -namespace OpenMcdf.Ole +namespace OpenMcdf.Ole; + +public interface IDictionaryProperty : IProperty { - public interface IDictionaryProperty : IProperty - { - } } \ No newline at end of file diff --git a/OpenMcdf.Ole/ITypedPropertyValue.cs b/OpenMcdf.Ole/ITypedPropertyValue.cs index 9c316265..76198997 100644 --- a/OpenMcdf.Ole/ITypedPropertyValue.cs +++ b/OpenMcdf.Ole/ITypedPropertyValue.cs @@ -1,21 +1,20 @@ -namespace OpenMcdf.Ole +namespace OpenMcdf.Ole; + +public interface ITypedPropertyValue : IProperty { - public interface ITypedPropertyValue : IProperty + VTPropertyType VTType { - VTPropertyType VTType - { - get; - //set; - } + get; + //set; + } - PropertyDimensions PropertyDimensions - { - get; - } + PropertyDimensions PropertyDimensions + { + get; + } - bool IsVariant - { - get; - } + bool IsVariant + { + get; } } diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index 073afbdf..9191cd70 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf.Ole; +using OpenMcdf3; + +namespace OpenMcdf.Ole; public enum ContainerType { @@ -84,7 +86,7 @@ public OlePropertiesContainer(int codePage, ContainerType containerType) ContainerType = containerType; } - internal OlePropertiesContainer(Stream cfStream) + public OlePropertiesContainer(CfbStream cfStream) { PropertySetStream pStream = new(); diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj index b8f832e4..219e3058 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj +++ b/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj @@ -2,7 +2,7 @@ net8.0-windows - 11.0 + 12.0 Exe enable enable diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf3.Tests/StorageTests.cs index 1160186b..de979cf4 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf3.Tests/StorageTests.cs @@ -12,7 +12,7 @@ public void Read(string fileName, long storageCount) { using (var rootStorage = RootStorage.OpenRead(fileName)) { - IEnumerable storageEntries = rootStorage.EnumerateEntries(StorageType.Storage); + IEnumerable storageEntries = rootStorage.EnumerateEntries(); Assert.AreEqual(storageCount, storageEntries.Count()); } diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 6ddb516e..9c357779 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorage", "Struct EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Ole", "OpenMcdf.Ole\OpenMcdf.Ole.csproj", "{06FFA945-128E-43FA-B541-38987BC1E0D5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorageExplorer", "StructuredStorageExplorer\StructuredStorageExplorer.csproj", "{D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {06FFA945-128E-43FA-B541-38987BC1E0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {06FFA945-128E-43FA-B541-38987BC1E0D5}.Release|Any CPU.Build.0 = Release|Any CPU + {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf3/CfbStream.cs index f030356b..5d076bfd 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf3/CfbStream.cs @@ -25,6 +25,8 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + public EntryInfo EntryInfo => directoryEntry.ToEntryInfo(); + public override bool CanRead => stream.CanRead; public override bool CanSeek => stream.CanSeek; diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index 081fc99c..dadf45df 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -6,7 +6,7 @@ namespace OpenMcdf3; /// /// The storage type of a . /// -public enum StorageType +enum StorageType { Unallocated = 0, Storage = 1, @@ -207,7 +207,14 @@ public void Recycle(StorageType storageType, string name) } } - public EntryInfo ToEntryInfo() => new(NameString, StreamLength); + public EntryType EntryType => Type switch + { + StorageType.Stream => EntryType.Stream, + StorageType.Storage => EntryType.Storage, + _ => throw new InvalidOperationException("Invalid storage type.") + }; + + public EntryInfo ToEntryInfo() => new(EntryType, NameString, StreamLength, CLSID, CreationTime, ModifiedTime); public override string ToString() => $"{Id}: \"{NameString}\""; diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf3/EntryInfo.cs index 87e8f607..c3311498 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf3/EntryInfo.cs @@ -1,6 +1,18 @@ namespace OpenMcdf3; +public enum EntryType +{ + Storage, + Stream, +} + /// /// Encapsulates information about an entry in a . /// -public readonly record struct EntryInfo(string Name, long Length); +public readonly record struct EntryInfo( + EntryType Type, + string Name, + long Length, + Guid CLSID, + DateTime CreationTime, + DateTime ModifiedTime); diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf3/Storage.cs index e7a77572..fa3f9722 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf3/Storage.cs @@ -20,6 +20,8 @@ internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) DirectoryEntry = directoryEntry; } + public EntryInfo EntryInfo => DirectoryEntry.ToEntryInfo(); + public IEnumerable EnumerateEntries() { this.ThrowIfDisposed(ioContext.IsDisposed); @@ -28,14 +30,6 @@ public IEnumerable EnumerateEntries() .Select(e => e.ToEntryInfo()); } - public IEnumerable EnumerateEntries(StorageType type) - { - this.ThrowIfDisposed(ioContext.IsDisposed); - - return EnumerateDirectoryEntries(type) - .Select(e => e.ToEntryInfo()); - } - IEnumerable EnumerateDirectoryEntries() { using DirectoryTreeEnumerator treeEnumerator = new(ioContext.DirectoryEntries, DirectoryEntry); diff --git a/OpenMcdf3/System/Index.cs b/OpenMcdf3/System/Index.cs index 52a556be..e99045e8 100644 --- a/OpenMcdf3/System/Index.cs +++ b/OpenMcdf3/System/Index.cs @@ -4,164 +4,163 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace System -{ - /// Represent a type can be used to index a collection either from the start or the end. - /// - /// Index is used by the C# compiler to support the new index syntax - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; - /// int lastElement = someArray[^1]; // lastElement = 5 - /// - /// +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - readonly struct Index : IEquatable +readonly struct Index : IEquatable +{ + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) { - private readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) + if (value < 0) { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } - - if (fromEnd) - _value = ~value; - else - _value = value; + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } - // The following private constructors mainly created for perf reason to avoid the checks - private Index(int value) - { + if (fromEnd) + _value = ~value; + else _value = value; - } + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } - /// Create an Index pointing at first element. - public static Index Start => new Index(0); + /// Create an Index pointing at first element. + public static Index Start => new Index(0); - /// Create an Index pointing at beyond last element. - public static Index End => new Index(~0); + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } - - return new Index(value); + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) - { - if (value < 0) - { - ThrowValueArgumentOutOfRange_NeedNonNegNumException(); - } + return new Index(value); + } - return new Index(~value); + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } - /// Returns the index value. - public int Value + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get { - get - { - if (_value < 0) - return ~_value; - else - return _value; - } + if (_value < 0) + return ~_value; + else + return _value; } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) { - int offset = _value; - if (IsFromEnd) - { - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - - offset += length + 1; - } - return offset; + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; } + return offset; + } - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() - { - if (IsFromEnd) - return ToStringFromEnd(); + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); - return ((uint)Value).ToString(); - } + return ((uint)Value).ToString(); + } - private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() - { + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { #if SYSTEM_PRIVATE_CORELIB - throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); #else - throw new ArgumentOutOfRangeException("value", "value must be non-negative"); + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); #endif - } + } - private string ToStringFromEnd() - { + private string ToStringFromEnd() + { #if (!NETSTANDARD2_0 && !NETFRAMEWORK) - Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value - bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); - Debug.Assert(formatted); - span[0] = '^'; - return new string(span.Slice(0, charsWritten + 1)); + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); #else - return '^' + Value.ToString(); + return '^' + Value.ToString(); #endif - } } } \ No newline at end of file diff --git a/OpenMcdf3/System/NullableAttributes.cs b/OpenMcdf3/System/NullableAttributes.cs index 08417f13..d62a560e 100644 --- a/OpenMcdf3/System/NullableAttributes.cs +++ b/OpenMcdf3/System/NullableAttributes.cs @@ -1,201 +1,200 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Diagnostics.CodeAnalysis -{ +namespace System.Diagnostics.CodeAnalysis; + #if !NETSTANDARD2_1 - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +/// Specifies that null is allowed as an input even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class AllowNullAttribute : Attribute - { } + sealed class AllowNullAttribute : Attribute +{ } - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +/// Specifies that null is disallowed as an input even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class DisallowNullAttribute : Attribute - { } + sealed class DisallowNullAttribute : Attribute +{ } - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +/// Specifies that an output may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class MaybeNullAttribute : Attribute - { } + sealed class MaybeNullAttribute : Attribute +{ } - /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class NotNullAttribute : Attribute - { } + sealed class NotNullAttribute : Attribute +{ } - /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +/// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that the output will be non-null if the named parameter is non-null. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class NotNullIfNotNullAttribute : Attribute - { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; - - /// Gets the associated parameter name. - public string ParameterName { get; } - } - - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] + sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } +} + +/// Applied to a method that will never return under any circumstance. +[AttributeUsage(AttributeTargets.Method, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class DoesNotReturnAttribute : Attribute - { } + sealed class DoesNotReturnAttribute : Attribute +{ } - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +/// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class DoesNotReturnIfAttribute : Attribute - { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets the condition parameter value. - public bool ParameterValue { get; } - } + sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } +} #endif - /// Specifies that the method or property will ensure that the listed field and property members have not-null values. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class MemberNotNullAttribute : Attribute - { - /// Initializes the attribute with a field or property member. - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullAttribute(string member) => Members = [member]; - - /// Initializes the attribute with the list of field and property members. - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullAttribute(params string[] members) => Members = members; - - /// Gets field or property member names. - public string[] Members { get; } - } - - /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - sealed class MemberNotNullWhenAttribute : Attribute + sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated field or property member will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated field and property members will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated field or property member will not be null. - /// - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = [member]; - } - - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated field and property members will not be null. - /// - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - - /// Gets field or property member names. - public string[] Members { get; } + ReturnValue = returnValue; + Members = members; } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } } \ No newline at end of file diff --git a/OpenMcdf3/System/Range.cs b/OpenMcdf3/System/Range.cs index 8967a17a..7cb53760 100644 --- a/OpenMcdf3/System/Range.cs +++ b/OpenMcdf3/System/Range.cs @@ -7,122 +7,121 @@ #if NETSTANDARD2_0 || NETFRAMEWORK #endif -namespace System -{ - /// Represent a range has start and end indexes. - /// - /// Range is used by the C# compiler to support the range syntax. - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; - /// int[] subArray1 = someArray[0..2]; // { 1, 2 } - /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } - /// - /// +namespace System; + +/// Represent a range has start and end indexes. +/// +/// Range is used by the C# compiler to support the range syntax. +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; +/// int[] subArray1 = someArray[0..2]; // { 1, 2 } +/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } +/// +/// #if SYSTEM_PRIVATE_CORELIB - public +public #else - internal +internal #endif - readonly struct Range : IEquatable +readonly struct Range : IEquatable +{ + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) { - /// Represent the inclusive start index of the Range. - public Index Start { get; } + Start = start; + End = end; + } - /// Represent the exclusive end index of the Range. - public Index End { get; } + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) - { - Start = start; - End = end; - } + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals([NotNullWhen(true)] object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); + } - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; - /// Returns the hash code for this instance. - public override int GetHashCode() + if (Start.IsFromEnd) { - return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); + span[0] = '^'; + pos = 1; } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() + if (End.IsFromEnd) { -#if (!NETSTANDARD2_0 && !NETFRAMEWORK) - Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint - int pos = 0; - - if (Start.IsFromEnd) - { - span[0] = '^'; - pos = 1; - } - bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); - Debug.Assert(formatted); - pos += charsWritten; - - span[pos++] = '.'; - span[pos++] = '.'; - - if (End.IsFromEnd) - { - span[pos++] = '^'; - } - formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); - Debug.Assert(formatted); - pos += charsWritten; - - return new string(span.Slice(0, pos)); -#else - return Start.ToString() + ".." + End.ToString(); -#endif + span[pos++] = '^'; } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + Debug.Assert(formatted); + pos += charsWritten; - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new Range(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new Range(Index.Start, end); + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } - /// Create a Range object starting from first element to the end. - public static Range All => new Range(Index.Start, Index.End); + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) - { - int start = Start.GetOffset(length); - int end = End.GetOffset(length); + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); - if ((uint)end > (uint)length || (uint)start > (uint)end) - { - ThrowArgumentOutOfRangeException(); - } + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); - return (start, end - start); - } + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); - private static void ThrowArgumentOutOfRangeException() + if ((uint)end > (uint)length || (uint)start > (uint)end) { - throw new ArgumentOutOfRangeException("length"); + ThrowArgumentOutOfRangeException(); } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); } } \ No newline at end of file diff --git a/StructuredStorageExplorer/MainForm.Designer.cs b/StructuredStorageExplorer/MainForm.Designer.cs new file mode 100644 index 00000000..a20ed86b --- /dev/null +++ b/StructuredStorageExplorer/MainForm.Designer.cs @@ -0,0 +1,466 @@ +namespace StructuredStorageExplorer +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); + this.treeView1 = new System.Windows.Forms.TreeView(); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.importDataStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.exportDataToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.addStorageStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.addStreamToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.saveFileDialog1 = new System.Windows.Forms.SaveFileDialog(); + this.menuStrip1 = new System.Windows.Forms.MenuStrip(); + this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.openFileMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.newStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.closeStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); + this.updateCurrentFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.editToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.preferencesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.openDataFileDialog = new System.Windows.Forms.OpenFileDialog(); + this.statusStrip1 = new System.Windows.Forms.StatusStrip(); + this.fileNameLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.propertyGrid1 = new System.Windows.Forms.PropertyGrid(); + this.splitContainer2 = new System.Windows.Forms.SplitContainer(); + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.tabPage1 = new System.Windows.Forms.TabPage(); + this.hexEditor = new Be.Windows.Forms.HexBox(); + this.tabPage2 = new System.Windows.Forms.TabPage(); + this.splitContainer3 = new System.Windows.Forms.SplitContainer(); + this.dgvOLEProps = new System.Windows.Forms.DataGridView(); + this.dgvUserDefinedProperties = new System.Windows.Forms.DataGridView(); + this.contextMenuStrip1.SuspendLayout(); + this.menuStrip1.SuspendLayout(); + this.statusStrip1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).BeginInit(); + this.splitContainer2.Panel1.SuspendLayout(); + this.splitContainer2.Panel2.SuspendLayout(); + this.splitContainer2.SuspendLayout(); + this.tabControl1.SuspendLayout(); + this.tabPage1.SuspendLayout(); + this.tabPage2.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).BeginInit(); + this.splitContainer3.Panel1.SuspendLayout(); + this.splitContainer3.Panel2.SuspendLayout(); + this.splitContainer3.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.dgvOLEProps)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.dgvUserDefinedProperties)).BeginInit(); + this.SuspendLayout(); + // + // openFileDialog1 + // + this.openFileDialog1.Filter = "Office files (*.xls *.doc *.ppt)|*.xls;*.doc;*.ppt|Thumbs db files (Thumbs.db)|*." + + "db|MSI Setup files (*.msi)|*.msi|Advanced Authoring Format (*.aaf)|*.aaf|All fi" + + "les (*.*)|*.*"; + this.openFileDialog1.Title = "Open OLE Structured Storage file"; + // + // treeView1 + // + this.treeView1.ContextMenuStrip = this.contextMenuStrip1; + this.treeView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeView1.HideSelection = false; + this.treeView1.Location = new System.Drawing.Point(0, 0); + this.treeView1.Name = "treeView1"; + this.treeView1.Size = new System.Drawing.Size(281, 201); + this.treeView1.TabIndex = 4; + this.treeView1.MouseUp += new System.Windows.Forms.MouseEventHandler(this.treeView1_MouseUp); + // + // contextMenuStrip1 + // + this.contextMenuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); + this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.importDataStripMenuItem1, + this.exportDataToolStripMenuItem, + this.addStorageStripMenuItem1, + this.addStreamToolStripMenuItem, + this.removeToolStripMenuItem}); + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(148, 114); + this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.contextMenuStrip1_Opening); + // + // importDataStripMenuItem1 + // + this.importDataStripMenuItem1.Name = "importDataStripMenuItem1"; + this.importDataStripMenuItem1.Size = new System.Drawing.Size(147, 22); + this.importDataStripMenuItem1.Text = "Import data..."; + this.importDataStripMenuItem1.Click += new System.EventHandler(this.importDataStripMenuItem1_Click); + // + // exportDataToolStripMenuItem + // + this.exportDataToolStripMenuItem.Name = "exportDataToolStripMenuItem"; + this.exportDataToolStripMenuItem.Size = new System.Drawing.Size(147, 22); + this.exportDataToolStripMenuItem.Text = "Export data..."; + this.exportDataToolStripMenuItem.Click += new System.EventHandler(this.exportDataToolStripMenuItem_Click); + // + // addStorageStripMenuItem1 + // + this.addStorageStripMenuItem1.Name = "addStorageStripMenuItem1"; + this.addStorageStripMenuItem1.Size = new System.Drawing.Size(147, 22); + this.addStorageStripMenuItem1.Text = "Add storage..."; + this.addStorageStripMenuItem1.Click += new System.EventHandler(this.addStorageStripMenuItem1_Click); + // + // addStreamToolStripMenuItem + // + this.addStreamToolStripMenuItem.Name = "addStreamToolStripMenuItem"; + this.addStreamToolStripMenuItem.Size = new System.Drawing.Size(147, 22); + this.addStreamToolStripMenuItem.Text = "Add stream..."; + this.addStreamToolStripMenuItem.Click += new System.EventHandler(this.addStreamToolStripMenuItem_Click); + // + // removeToolStripMenuItem + // + this.removeToolStripMenuItem.Name = "removeToolStripMenuItem"; + this.removeToolStripMenuItem.Size = new System.Drawing.Size(147, 22); + this.removeToolStripMenuItem.Text = "Remove"; + this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click); + // + // saveFileDialog1 + // + this.saveFileDialog1.DefaultExt = "*.bin"; + this.saveFileDialog1.Filter = "Exported data files (*.bin)|*.bin|All files (*.*)|*.*"; + // + // menuStrip1 + // + this.menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); + this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.fileToolStripMenuItem, + this.editToolStripMenuItem}); + this.menuStrip1.Location = new System.Drawing.Point(0, 0); + this.menuStrip1.Name = "menuStrip1"; + this.menuStrip1.Padding = new System.Windows.Forms.Padding(6, 1, 0, 1); + this.menuStrip1.Size = new System.Drawing.Size(853, 24); + this.menuStrip1.TabIndex = 5; + this.menuStrip1.Text = "menuStrip1"; + // + // fileToolStripMenuItem + // + this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.openFileMenuItem, + this.newStripMenuItem1, + this.closeStripMenuItem1, + this.toolStripSeparator2, + this.updateCurrentFileToolStripMenuItem, + this.saveAsToolStripMenuItem}); + this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; + this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 22); + this.fileToolStripMenuItem.Text = "File"; + // + // openFileMenuItem + // + this.openFileMenuItem.Image = global::StructuredStorageExplorer.Properties.Resources.folder; + this.openFileMenuItem.Name = "openFileMenuItem"; + this.openFileMenuItem.Size = new System.Drawing.Size(187, 26); + this.openFileMenuItem.Text = "Open..."; + this.openFileMenuItem.Click += new System.EventHandler(this.openFileMenuItem_Click); + // + // newStripMenuItem1 + // + this.newStripMenuItem1.Image = global::StructuredStorageExplorer.Properties.Resources.page_white; + this.newStripMenuItem1.Name = "newStripMenuItem1"; + this.newStripMenuItem1.Size = new System.Drawing.Size(187, 26); + this.newStripMenuItem1.Text = "New Compound File"; + this.newStripMenuItem1.Click += new System.EventHandler(this.newStripMenuItem1_Click); + // + // closeStripMenuItem1 + // + this.closeStripMenuItem1.Name = "closeStripMenuItem1"; + this.closeStripMenuItem1.Size = new System.Drawing.Size(187, 26); + this.closeStripMenuItem1.Text = "Close file"; + this.closeStripMenuItem1.Click += new System.EventHandler(this.closeStripMenuItem1_Click); + // + // toolStripSeparator2 + // + this.toolStripSeparator2.Name = "toolStripSeparator2"; + this.toolStripSeparator2.Size = new System.Drawing.Size(184, 6); + // + // updateCurrentFileToolStripMenuItem + // + this.updateCurrentFileToolStripMenuItem.Image = global::StructuredStorageExplorer.Properties.Resources.disk; + this.updateCurrentFileToolStripMenuItem.Name = "updateCurrentFileToolStripMenuItem"; + this.updateCurrentFileToolStripMenuItem.Size = new System.Drawing.Size(187, 26); + this.updateCurrentFileToolStripMenuItem.Text = "Save"; + this.updateCurrentFileToolStripMenuItem.Click += new System.EventHandler(this.updateCurrentFileToolStripMenuItem_Click); + // + // saveAsToolStripMenuItem + // + this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; + this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(187, 26); + this.saveAsToolStripMenuItem.Text = "Save As..."; + this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click); + // + // editToolStripMenuItem + // + this.editToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.preferencesToolStripMenuItem}); + this.editToolStripMenuItem.Name = "editToolStripMenuItem"; + this.editToolStripMenuItem.Size = new System.Drawing.Size(39, 22); + this.editToolStripMenuItem.Text = "Edit"; + // + // preferencesToolStripMenuItem + // + this.preferencesToolStripMenuItem.Name = "preferencesToolStripMenuItem"; + this.preferencesToolStripMenuItem.Size = new System.Drawing.Size(135, 22); + this.preferencesToolStripMenuItem.Text = "Preferences"; + this.preferencesToolStripMenuItem.Click += new System.EventHandler(this.preferencesToolStripMenuItem_Click); + // + // statusStrip1 + // + this.statusStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); + this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.fileNameLabel}); + this.statusStrip1.Location = new System.Drawing.Point(0, 436); + this.statusStrip1.Name = "statusStrip1"; + this.statusStrip1.Size = new System.Drawing.Size(853, 22); + this.statusStrip1.TabIndex = 6; + this.statusStrip1.Text = "statusStrip1"; + // + // fileNameLabel + // + this.fileNameLabel.Name = "fileNameLabel"; + this.fileNameLabel.Size = new System.Drawing.Size(0, 17); + // + // splitContainer1 + // + this.splitContainer1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(0, 0); + this.splitContainer1.Name = "splitContainer1"; + this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.treeView1); + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.propertyGrid1); + this.splitContainer1.Size = new System.Drawing.Size(283, 412); + this.splitContainer1.SplitterDistance = 203; + this.splitContainer1.TabIndex = 5; + // + // propertyGrid1 + // + this.propertyGrid1.Dock = System.Windows.Forms.DockStyle.Fill; + this.propertyGrid1.Location = new System.Drawing.Point(0, 0); + this.propertyGrid1.Name = "propertyGrid1"; + this.propertyGrid1.Size = new System.Drawing.Size(281, 203); + this.propertyGrid1.TabIndex = 0; + this.propertyGrid1.ToolbarVisible = false; + // + // splitContainer2 + // + this.splitContainer2.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer2.Location = new System.Drawing.Point(0, 24); + this.splitContainer2.Name = "splitContainer2"; + // + // splitContainer2.Panel1 + // + this.splitContainer2.Panel1.Controls.Add(this.splitContainer1); + // + // splitContainer2.Panel2 + // + this.splitContainer2.Panel2.Controls.Add(this.tabControl1); + this.splitContainer2.Size = new System.Drawing.Size(853, 412); + this.splitContainer2.SplitterDistance = 283; + this.splitContainer2.TabIndex = 7; + // + // tabControl1 + // + this.tabControl1.Controls.Add(this.tabPage1); + this.tabControl1.Controls.Add(this.tabPage2); + this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControl1.Location = new System.Drawing.Point(0, 0); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(566, 412); + this.tabControl1.TabIndex = 1; + // + // tabPage1 + // + this.tabPage1.Controls.Add(this.hexEditor); + this.tabPage1.Location = new System.Drawing.Point(4, 22); + this.tabPage1.Name = "tabPage1"; + this.tabPage1.Padding = new System.Windows.Forms.Padding(3, 3, 3, 3); + this.tabPage1.Size = new System.Drawing.Size(558, 386); + this.tabPage1.TabIndex = 0; + this.tabPage1.Text = "Raw Data"; + this.tabPage1.UseVisualStyleBackColor = true; + // + // hexEditor + // + this.hexEditor.BackColor = System.Drawing.Color.WhiteSmoke; + this.hexEditor.Dock = System.Windows.Forms.DockStyle.Fill; + this.hexEditor.Font = new System.Drawing.Font("Courier New", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.hexEditor.InfoForeColor = System.Drawing.Color.Empty; + this.hexEditor.LineInfoVisible = true; + this.hexEditor.Location = new System.Drawing.Point(3, 3); + this.hexEditor.Name = "hexEditor"; + this.hexEditor.ShadowSelectionColor = System.Drawing.Color.FromArgb(((int)(((byte)(100)))), ((int)(((byte)(60)))), ((int)(((byte)(188)))), ((int)(((byte)(255))))); + this.hexEditor.Size = new System.Drawing.Size(552, 380); + this.hexEditor.StringViewVisible = true; + this.hexEditor.TabIndex = 0; + this.hexEditor.UseFixedBytesPerLine = true; + this.hexEditor.VScrollBarVisible = true; + // + // tabPage2 + // + this.tabPage2.Controls.Add(this.splitContainer3); + this.tabPage2.Location = new System.Drawing.Point(4, 22); + this.tabPage2.Name = "tabPage2"; + this.tabPage2.Padding = new System.Windows.Forms.Padding(3, 3, 3, 3); + this.tabPage2.Size = new System.Drawing.Size(558, 396); + this.tabPage2.TabIndex = 1; + this.tabPage2.Text = "OLE Properties"; + this.tabPage2.UseVisualStyleBackColor = true; + // + // splitContainer3 + // + this.splitContainer3.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer3.Location = new System.Drawing.Point(3, 3); + this.splitContainer3.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); + this.splitContainer3.Name = "splitContainer3"; + this.splitContainer3.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer3.Panel1 + // + this.splitContainer3.Panel1.Controls.Add(this.dgvOLEProps); + // + // splitContainer3.Panel2 + // + this.splitContainer3.Panel2.Controls.Add(this.dgvUserDefinedProperties); + this.splitContainer3.Size = new System.Drawing.Size(552, 390); + this.splitContainer3.SplitterDistance = 195; + this.splitContainer3.SplitterWidth = 3; + this.splitContainer3.TabIndex = 2; + // + // dgvOLEProps + // + this.dgvOLEProps.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dgvOLEProps.Dock = System.Windows.Forms.DockStyle.Fill; + this.dgvOLEProps.Location = new System.Drawing.Point(0, 0); + this.dgvOLEProps.Name = "dgvOLEProps"; + this.dgvOLEProps.RowHeadersWidth = 62; + this.dgvOLEProps.Size = new System.Drawing.Size(552, 195); + this.dgvOLEProps.TabIndex = 0; + // + // dgvUserDefinedProperties + // + this.dgvUserDefinedProperties.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dgvUserDefinedProperties.Dock = System.Windows.Forms.DockStyle.Fill; + this.dgvUserDefinedProperties.Location = new System.Drawing.Point(0, 0); + this.dgvUserDefinedProperties.Name = "dgvUserDefinedProperties"; + this.dgvUserDefinedProperties.RowHeadersWidth = 62; + this.dgvUserDefinedProperties.Size = new System.Drawing.Size(552, 192); + this.dgvUserDefinedProperties.TabIndex = 1; + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(853, 458); + this.Controls.Add(this.splitContainer2); + this.Controls.Add(this.statusStrip1); + this.Controls.Add(this.menuStrip1); + this.MainMenuStrip = this.menuStrip1; + this.Name = "MainForm"; + this.Text = "Structured Storage eXplorer"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + this.contextMenuStrip1.ResumeLayout(false); + this.menuStrip1.ResumeLayout(false); + this.menuStrip1.PerformLayout(); + this.statusStrip1.ResumeLayout(false); + this.statusStrip1.PerformLayout(); + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.splitContainer2.Panel1.ResumeLayout(false); + this.splitContainer2.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).EndInit(); + this.splitContainer2.ResumeLayout(false); + this.tabControl1.ResumeLayout(false); + this.tabPage1.ResumeLayout(false); + this.tabPage2.ResumeLayout(false); + this.splitContainer3.Panel1.ResumeLayout(false); + this.splitContainer3.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).EndInit(); + this.splitContainer3.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.dgvOLEProps)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.dgvUserDefinedProperties)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.OpenFileDialog openFileDialog1; + private System.Windows.Forms.TreeView treeView1; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.ToolStripMenuItem exportDataToolStripMenuItem; + private System.Windows.Forms.SaveFileDialog saveFileDialog1; + private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; + private System.Windows.Forms.MenuStrip menuStrip1; + private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem updateCurrentFileToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem addStreamToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem importDataStripMenuItem1; + private System.Windows.Forms.OpenFileDialog openDataFileDialog; + private System.Windows.Forms.ToolStripMenuItem addStorageStripMenuItem1; + private System.Windows.Forms.ToolStripMenuItem newStripMenuItem1; + private System.Windows.Forms.ToolStripMenuItem openFileMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; + private System.Windows.Forms.StatusStrip statusStrip1; + private System.Windows.Forms.ToolStripStatusLabel fileNameLabel; + private System.Windows.Forms.SplitContainer splitContainer1; + private System.Windows.Forms.PropertyGrid propertyGrid1; + private System.Windows.Forms.SplitContainer splitContainer2; + private Be.Windows.Forms.HexBox hexEditor; + private System.Windows.Forms.ToolStripMenuItem closeStripMenuItem1; + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage tabPage1; + private System.Windows.Forms.TabPage tabPage2; + private System.Windows.Forms.DataGridView dgvOLEProps; + private System.Windows.Forms.DataGridView dgvUserDefinedProperties; + private System.Windows.Forms.SplitContainer splitContainer3; + private System.Windows.Forms.ToolStripMenuItem editToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem preferencesToolStripMenuItem; + } +} + diff --git a/StructuredStorageExplorer/MainForm.cs b/StructuredStorageExplorer/MainForm.cs new file mode 100644 index 00000000..812af78d --- /dev/null +++ b/StructuredStorageExplorer/MainForm.cs @@ -0,0 +1,487 @@ +#define OLE_PROPERTY + +using OpenMcdf.Ole; +using OpenMcdf3; +using StructuredStorageExplorer.Properties; +using System.Collections; +using System.ComponentModel; +using System.Data; +using System.Globalization; + +// Author Federico Blaseotto + +namespace StructuredStorageExplorer; + +record class NodeSelection(Storage Parent, EntryInfo EntryInfo); + +/// +/// Sample Structured Storage viewer to +/// demonstrate use of OpenMCDF +/// +public partial class MainForm : Form +{ + private RootStorage? cf; + private FileStream? fs; + private bool canUpdate; + + public MainForm() + { + InitializeComponent(); + +#if !OLE_PROPERTY + tabControl1.TabPages.Remove(tabPage2); +#endif + + //Load images for icons from resx + Image folderImage = (Image)Resources.ResourceManager.GetObject("storage"); + Image streamImage = (Image)Resources.ResourceManager.GetObject("stream"); + //Image olePropsImage = (Image)Properties.Resources.ResourceManager.GetObject("oleprops"); + + treeView1.ImageList = new ImageList(); + treeView1.ImageList.Images.Add(folderImage); + treeView1.ImageList.Images.Add(streamImage); + //treeView1.ImageList.Images.Add(olePropsImage); + + saveAsToolStripMenuItem.Enabled = false; + updateCurrentFileToolStripMenuItem.Enabled = false; + } + + private void OpenFile() + { + if (!string.IsNullOrEmpty(openFileDialog1.FileName)) + { + CloseCurrentFile(); + + treeView1.Nodes.Clear(); + fileNameLabel.Text = openFileDialog1.FileName; + LoadFile(openFileDialog1.FileName, true); + canUpdate = true; + saveAsToolStripMenuItem.Enabled = true; + updateCurrentFileToolStripMenuItem.Enabled = true; + } + } + + private void CloseCurrentFile() + { + cf?.Dispose(); + cf = null; + + fs?.Close(); + fs = null; + + treeView1.Nodes.Clear(); + fileNameLabel.Text = string.Empty; + saveAsToolStripMenuItem.Enabled = false; + updateCurrentFileToolStripMenuItem.Enabled = false; + + propertyGrid1.SelectedObject = null; + hexEditor.ByteProvider = null; + +#if OLE_PROPERTY + dgvUserDefinedProperties.DataSource = null; + dgvOLEProps.DataSource = null; +#endif + } + + private void CreateNewFile() + { + CloseCurrentFile(); + + cf = RootStorage.Create(Path.GetTempFileName()); + canUpdate = false; + saveAsToolStripMenuItem.Enabled = true; + + updateCurrentFileToolStripMenuItem.Enabled = false; + + RefreshTree(); + } + + private void RefreshTree() + { + treeView1.Nodes.Clear(); + TreeNode root = treeView1.Nodes.Add("Root Entry", "Root"); + root.ImageIndex = 0; + root.Tag = new NodeSelection(null, cf.EntryInfo); + + // Recursive function to get all storage and streams + AddNodes(root, cf); + } + + private void LoadFile(string fileName, bool enableCommit) + { + fs = new FileStream( + fileName, + FileMode.Open, + enableCommit ? + FileAccess.ReadWrite + : FileAccess.Read); + + try + { + cf?.Dispose(); + cf = null; + + // Load file + cf = RootStorage.Open(fs, enableCommit ? StorageModeFlags.Transacted : StorageModeFlags.None); + + RefreshTree(); + } + catch (Exception ex) + { + cf?.Dispose(); + cf = null; + + fs?.Close(); + fs = null; + + treeView1.Nodes.Clear(); + fileNameLabel.Text = string.Empty; + MessageBox.Show("Internal error: " + ex.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + /// + /// Recursive addition of tree nodes foreach child of current item in the storage + /// + /// Current TreeNode + /// Current storage associated with node + private static void AddNodes(TreeNode node, Storage storage) + { + foreach (EntryInfo item in storage.EnumerateEntries()) + { + TreeNode childNode = node.Nodes.Add(item.Name); + + childNode.Tag = new NodeSelection(storage, item); + + if (item.Type is EntryType.Storage) + { + // Storage + childNode.ImageIndex = 0; + childNode.SelectedImageIndex = 0; + + Storage subStorage = storage.OpenStorage(item.Name); + // Recursion into the storage + AddNodes(childNode, subStorage); + } + else + { + // Stream + childNode.ImageIndex = 1; + childNode.SelectedImageIndex = 1; + } + } + } + + private void exportDataToolStripMenuItem_Click(object sender, EventArgs e) + { + // No export if storage + NodeSelection? selection = treeView1.SelectedNode?.Tag as NodeSelection; + if (selection is null || selection.EntryInfo.Type is not EntryType.Stream) + { + MessageBox.Show("Only stream data can be exported", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + // A lot of stream and storage have only non-printable characters. + // We need to sanitize filename. + + string sanitizedFileName = string.Empty; + + foreach (char c in selection.EntryInfo.Name) + { + UnicodeCategory category = char.GetUnicodeCategory(c); + if (category is UnicodeCategory.LetterNumber or UnicodeCategory.LowercaseLetter or UnicodeCategory.UppercaseLetter) + sanitizedFileName += c; + } + + if (string.IsNullOrEmpty(sanitizedFileName)) + { + sanitizedFileName = "tempFileName"; + } + + saveFileDialog1.FileName = $"{sanitizedFileName}.bin"; + + if (saveFileDialog1.ShowDialog() == DialogResult.OK) + { + try + { + using FileStream fs = new(saveFileDialog1.FileName, FileMode.CreateNew, FileAccess.ReadWrite); + using CfbStream cfbStream = selection.Parent.OpenStream(selection.EntryInfo.Name); + cfbStream.CopyTo(fs); + } + catch (Exception ex) + { + treeView1.Nodes.Clear(); + MessageBox.Show($"Internal error: {ex.Message}", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void removeToolStripMenuItem_Click(object sender, EventArgs e) + { + TreeNode n = treeView1.SelectedNode; + if (n?.Parent?.Tag is NodeSelection selection && selection.Parent is not null) + selection.Parent.Delete(selection.EntryInfo.Name); + + RefreshTree(); + } + + private void saveAsToolStripMenuItem_Click(object sender, EventArgs e) + { + saveFileDialog1.FilterIndex = 2; + if (saveFileDialog1.ShowDialog() == DialogResult.OK) + { + //cf.SaveAs(saveFileDialog1.FileName); // TODO + } + } + + private void updateCurrentFileToolStripMenuItem_Click(object sender, EventArgs e) + { + if (canUpdate) + { + if (hexEditor.ByteProvider is not null && hexEditor.ByteProvider.HasChanges()) + hexEditor.ByteProvider.ApplyChanges(); + cf.Commit(); + } + else + { + MessageBox.Show("Cannot update a compound document that is not based on a stream or on a file", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void addStreamToolStripMenuItem_Click(object sender, EventArgs e) + { + string streamName = string.Empty; + + if (Utils.InputBox("Add stream", "Insert stream name", ref streamName) == DialogResult.OK + && treeView1.SelectedNode.Tag is RootStorage storage) + { + try + { + storage.CreateStream(streamName); + } + catch (IOException ex) + { + MessageBox.Show($"Error creating stream: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + RefreshTree(); + } + } + + private void addStorageStripMenuItem1_Click(object sender, EventArgs e) + { + string storageName = string.Empty; + + if (Utils.InputBox("Add storage", "Insert storage name", ref storageName) == DialogResult.OK + && treeView1.SelectedNode.Tag is RootStorage storage) + { + try + { + storage.CreateStorage(storageName); + } + catch (IOException ex) + { + MessageBox.Show($"Error creating storage: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + RefreshTree(); + } + } + + private void importDataStripMenuItem1_Click(object sender, EventArgs e) + { + if (openDataFileDialog.ShowDialog() == DialogResult.OK + && treeView1.SelectedNode.Tag is CfbStream stream) + { + using FileStream f = new(openDataFileDialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read); + f.CopyTo(stream); + + RefreshTree(); + } + } + + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) + { + cf?.Dispose(); + cf = null; + } + + private void contextMenuStrip1_Opening(object sender, CancelEventArgs e) + { + } + + private void newStripMenuItem1_Click(object sender, EventArgs e) + { + CreateNewFile(); + } + + private void openFileMenuItem_Click(object sender, EventArgs e) + { + if (openFileDialog1.ShowDialog() == DialogResult.OK) + { + try + { + OpenFile(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + MessageBox.Show($"Cannot open file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void treeView1_MouseUp(object sender, MouseEventArgs e) + { + TreeNode n = treeView1.GetNodeAt(e.X, e.Y); + if (n.Tag is not NodeSelection nodeSelection) + { + addStorageStripMenuItem1.Enabled = true; + addStreamToolStripMenuItem.Enabled = true; + importDataStripMenuItem1.Enabled = false; + exportDataToolStripMenuItem.Enabled = false; + removeToolStripMenuItem.Enabled = false; + propertyGrid1.SelectedObject = null; + return; + } + + // Get the node under the mouse cursor. + // We intercept both left and right mouse clicks + // and set the selected TreeNode according. + try + { + if (hexEditor.ByteProvider is not null && hexEditor.ByteProvider.HasChanges()) + { + if (MessageBox.Show("Do you want to save pending changes?", "Save changes", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) + { + hexEditor.ByteProvider.ApplyChanges(); + } + } + + treeView1.SelectedNode = n; + + // The tag property contains the underlying CFItem. + //CFItem target = (CFItem)n.Tag; + + if (nodeSelection.EntryInfo.Type is EntryType.Stream) + { + using CfbStream stream = nodeSelection.Parent.OpenStream(nodeSelection.EntryInfo.Name); + addStorageStripMenuItem1.Enabled = false; + addStreamToolStripMenuItem.Enabled = false; + importDataStripMenuItem1.Enabled = true; + exportDataToolStripMenuItem.Enabled = true; + + hexEditor.ByteProvider = new StreamDataProvider(stream); + +#if OLE_PROPERTY + UpdateOleTab(stream); +#endif + } + else + { + hexEditor.ByteProvider = null; + } + + propertyGrid1.SelectedObject = nodeSelection.EntryInfo; + } + catch (Exception ex) + { + cf?.Dispose(); + cf = null; + + fs?.Close(); + fs = null; + + treeView1.Nodes.Clear(); + fileNameLabel.Text = string.Empty; + + MessageBox.Show($"Internal error: {ex.Message}", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void UpdateOleTab(CfbStream stream) + { + dgvUserDefinedProperties.DataSource = null; + dgvOLEProps.DataSource = null; + + if (stream.EntryInfo.Name is "\u0005SummaryInformation" or "\u0005DocumentSummaryInformation") + { + OlePropertiesContainer c = new(stream); + + DataTable ds = new(); + ds.Columns.Add("Name", typeof(string)); + ds.Columns.Add("Type", typeof(string)); + ds.Columns.Add("Value", typeof(string)); + + foreach (OleProperty p in c.Properties) + { + if (p.Value is not byte[] and IList list) + { + for (int h = 0; h < list.Count; h++) + { + DataRow dr = ds.NewRow(); + dr.ItemArray = [p.PropertyName, p.VTType, list[h]]; + ds.Rows.Add(dr); + } + } + else + { + DataRow dr = ds.NewRow(); + dr.ItemArray = [p.PropertyName, p.VTType, p.Value]; + ds.Rows.Add(dr); + } + } + + ds.AcceptChanges(); + dgvOLEProps.DataSource = ds; + + if (c.HasUserDefinedProperties) + { + DataTable ds2 = new(); + ds2.Columns.Add("Name", typeof(string)); + ds2.Columns.Add("Type", typeof(string)); + ds2.Columns.Add("Value", typeof(string)); + + foreach (OleProperty p in c.UserDefinedProperties.Properties) + { + if (p.Value is not byte[] and IList list) + { + for (int h = 0; h < list.Count; h++) + { + DataRow dr = ds2.NewRow(); + dr.ItemArray = [p.PropertyName, p.VTType, list[h]]; + ds2.Rows.Add(dr); + } + } + else + { + DataRow dr = ds2.NewRow(); + dr.ItemArray = [p.PropertyName, p.VTType, p.Value]; + ds2.Rows.Add(dr); + } + } + + ds2.AcceptChanges(); + dgvUserDefinedProperties.DataSource = ds2; + } + } + } + + private void closeStripMenuItem1_Click(object sender, EventArgs e) + { + if (hexEditor.ByteProvider is not null + && hexEditor.ByteProvider.HasChanges() + && MessageBox.Show("Do you want to save pending changes?", "Save changes", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) + { + hexEditor.ByteProvider.ApplyChanges(); + } + + CloseCurrentFile(); + } + + private void preferencesToolStripMenuItem_Click(object sender, EventArgs e) + { + using PreferencesForm pref = new(); + pref.ShowDialog(); + } +} diff --git a/StructuredStorageExplorer/MainForm.resx b/StructuredStorageExplorer/MainForm.resx new file mode 100644 index 00000000..f8835009 --- /dev/null +++ b/StructuredStorageExplorer/MainForm.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 147, 17 + + + 292, 17 + + + 420, 17 + + + 529, 17 + + + 676, 17 + + + 100 + + \ No newline at end of file diff --git a/StructuredStorageExplorer/OpenMcdf.snk b/StructuredStorageExplorer/OpenMcdf.snk new file mode 100644 index 0000000000000000000000000000000000000000..b2990e73c74b1b002bd693d3d8008085b96f461f GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097nwG6x-K;LO^j4j6cs`(%400Z5@yPQnZ7{$0%=kHi|-&%jj zIZo$C(^orbVlD(&H`PYI`!B&VSLuY?Q>Q)El2oao)5)5QriW~iFixpxnXCq=jeExP zwIDQlYjkbW{FAWhZ)4zsWDM5B0Ai$5KWtA3WU3fix^?x&19(Tr`ai`}{|vA3d;= zvaMECN!3mgvGE5t)=hH7N)+WzrP#-#HOb%u(+-Je%Npw`Uv^Y}ekuG=&$uUWso2C? z_`m=&4^8V#^8!SoI$f{Y;uM2Qdgc1+0wDpGERes_*oK=gF2S}Uz!p3<(}iN-sPo!x z + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.cbEnableValidation = new System.Windows.Forms.CheckBox(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.btnSavePreferences = new System.Windows.Forms.Button(); + this.btnCancelPreferences = new System.Windows.Forms.Button(); + this.groupBox1.SuspendLayout(); + this.SuspendLayout(); + // + // cbEnableValidation + // + this.cbEnableValidation.AutoSize = true; + this.cbEnableValidation.Location = new System.Drawing.Point(6, 25); + this.cbEnableValidation.Name = "cbEnableValidation"; + this.cbEnableValidation.Size = new System.Drawing.Size(277, 24); + this.cbEnableValidation.TabIndex = 0; + this.cbEnableValidation.Text = "File Validation Exceptions enabled"; + this.cbEnableValidation.UseVisualStyleBackColor = true; + // + // groupBox1 + // + this.groupBox1.Controls.Add(this.cbEnableValidation); + this.groupBox1.Location = new System.Drawing.Point(12, 12); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(555, 259); + this.groupBox1.TabIndex = 1; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Preferences"; + // + // btnSavePreferences + // + this.btnSavePreferences.Location = new System.Drawing.Point(473, 298); + this.btnSavePreferences.Name = "btnSavePreferences"; + this.btnSavePreferences.Size = new System.Drawing.Size(94, 30); + this.btnSavePreferences.TabIndex = 2; + this.btnSavePreferences.Text = "OK"; + this.btnSavePreferences.UseVisualStyleBackColor = true; + this.btnSavePreferences.Click += new System.EventHandler(this.btnSavePreferences_Click); + // + // btnCancelPreferences + // + this.btnCancelPreferences.Location = new System.Drawing.Point(373, 298); + this.btnCancelPreferences.Name = "btnCancelPreferences"; + this.btnCancelPreferences.Size = new System.Drawing.Size(94, 30); + this.btnCancelPreferences.TabIndex = 3; + this.btnCancelPreferences.Text = "Cancel"; + this.btnCancelPreferences.UseVisualStyleBackColor = true; + this.btnCancelPreferences.Click += new System.EventHandler(this.btnCancelPreferences_Click); + // + // PreferencesForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(579, 340); + this.Controls.Add(this.btnCancelPreferences); + this.Controls.Add(this.btnSavePreferences); + this.Controls.Add(this.groupBox1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Name = "PreferencesForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Preferences"; + this.Load += new System.EventHandler(this.PreferencesForm_Load); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.CheckBox cbEnableValidation; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.Button btnSavePreferences; + private System.Windows.Forms.Button btnCancelPreferences; + } +} \ No newline at end of file diff --git a/StructuredStorageExplorer/PreferencesForm.cs b/StructuredStorageExplorer/PreferencesForm.cs new file mode 100644 index 00000000..66db24e7 --- /dev/null +++ b/StructuredStorageExplorer/PreferencesForm.cs @@ -0,0 +1,31 @@ +using StructuredStorageExplorer.Properties; + +namespace StructuredStorageExplorer; + +public partial class PreferencesForm : Form +{ + public PreferencesForm() + { + InitializeComponent(); + } + + private void btnSavePreferences_Click(object sender, EventArgs e) + { + Settings.Default.EnableValidation = cbEnableValidation.Checked; + Settings.Default.Save(); + DialogResult = DialogResult.OK; + Close(); + } + + private void btnCancelPreferences_Click(object sender, EventArgs e) + { + cbEnableValidation.Checked = Settings.Default.EnableValidation; + DialogResult = DialogResult.Cancel; + Close(); + } + + private void PreferencesForm_Load(object sender, EventArgs e) + { + cbEnableValidation.Checked = Settings.Default.EnableValidation; + } +} diff --git a/StructuredStorageExplorer/PreferencesForm.resx b/StructuredStorageExplorer/PreferencesForm.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/StructuredStorageExplorer/PreferencesForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/StructuredStorageExplorer/Program.cs b/StructuredStorageExplorer/Program.cs new file mode 100644 index 00000000..b04337b9 --- /dev/null +++ b/StructuredStorageExplorer/Program.cs @@ -0,0 +1,15 @@ +namespace StructuredStorageExplorer; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } +} diff --git a/StructuredStorageExplorer/Properties/Resources.Designer.cs b/StructuredStorageExplorer/Properties/Resources.Designer.cs new file mode 100644 index 00000000..530092ea --- /dev/null +++ b/StructuredStorageExplorer/Properties/Resources.Designer.cs @@ -0,0 +1,123 @@ +//------------------------------------------------------------------------------ +// +// Il codice è stato generato da uno strumento. +// Versione runtime:4.0.30319.42000 +// +// Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se +// il codice viene rigenerato. +// +//------------------------------------------------------------------------------ + +namespace StructuredStorageExplorer.Properties { + using System; + + + /// + /// Classe di risorse fortemente tipizzata per la ricerca di stringhe localizzate e così via. + /// + // Questa classe è stata generata automaticamente dalla classe StronglyTypedResourceBuilder. + // tramite uno strumento quale ResGen o Visual Studio. + // Per aggiungere o rimuovere un membro, modificare il file con estensione ResX ed eseguire nuovamente ResGen + // con l'opzione /str oppure ricompilare il progetto VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Restituisce l'istanza di ResourceManager nella cache utilizzata da questa classe. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("StructuredStorageExplorer.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Esegue l'override della proprietà CurrentUICulture del thread corrente per tutte le + /// ricerche di risorse eseguite utilizzando questa classe di risorse fortemente tipizzata. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap disk { + get { + object obj = ResourceManager.GetObject("disk", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap door_out { + get { + object obj = ResourceManager.GetObject("door_out", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap folder { + get { + object obj = ResourceManager.GetObject("folder", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap page_white { + get { + object obj = ResourceManager.GetObject("page_white", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap storage { + get { + object obj = ResourceManager.GetObject("storage", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Cerca una risorsa localizzata di tipo System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap stream { + get { + object obj = ResourceManager.GetObject("stream", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/StructuredStorageExplorer/Properties/Resources.resx b/StructuredStorageExplorer/Properties/Resources.resx new file mode 100644 index 00000000..778d80b1 --- /dev/null +++ b/StructuredStorageExplorer/Properties/Resources.resx @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\img\disk.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\img\door_out.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\img\folder.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\img\page_white.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\img\storage.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\img\stream.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/StructuredStorageExplorer/Properties/Settings.Designer.cs b/StructuredStorageExplorer/Properties/Settings.Designer.cs new file mode 100644 index 00000000..ebbdce8a --- /dev/null +++ b/StructuredStorageExplorer/Properties/Settings.Designer.cs @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// Il codice è stato generato da uno strumento. +// Versione runtime:4.0.30319.42000 +// +// Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se +// il codice viene rigenerato. +// +//------------------------------------------------------------------------------ + +namespace StructuredStorageExplorer.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.7.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool CommitEnabled { + get { + return ((bool)(this["CommitEnabled"])); + } + set { + this["CommitEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool EnableValidation { + get { + return ((bool)(this["EnableValidation"])); + } + set { + this["EnableValidation"] = value; + } + } + } +} diff --git a/StructuredStorageExplorer/Properties/Settings.settings b/StructuredStorageExplorer/Properties/Settings.settings new file mode 100644 index 00000000..7c61266d --- /dev/null +++ b/StructuredStorageExplorer/Properties/Settings.settings @@ -0,0 +1,12 @@ + + + + + + True + + + False + + + \ No newline at end of file diff --git a/StructuredStorageExplorer/StreamDataProvider.cs b/StructuredStorageExplorer/StreamDataProvider.cs new file mode 100644 index 00000000..ffecb71d --- /dev/null +++ b/StructuredStorageExplorer/StreamDataProvider.cs @@ -0,0 +1,160 @@ +using Be.Windows.Forms; +using OpenMcdf3; + +namespace StructuredStorageExplorer; + +public class StreamDataProvider : IByteProvider +{ + /// + /// Modifying stream + /// + readonly CfbStream _modifiedStream; + + /// + /// Contains information about changes. + /// + bool _hasChanges; + + /// + /// Initializes a new instance of the StreamDataProvider class. + /// + /// + public StreamDataProvider(CfbStream modifiedStream) + { + byte[] data = new byte[modifiedStream.Length]; + modifiedStream.Position = 0; + modifiedStream.ReadExactly(data, 0, data.Length); + Bytes = new ByteCollection(data); + _modifiedStream = modifiedStream; + } + + /// + /// Raises the Changed event. + /// + void OnChanged(EventArgs e) + { + _hasChanges = true; + + Changed?.Invoke(this, e); + } + + /// + /// Raises the LengthChanged event. + /// + void OnLengthChanged(EventArgs e) + { + LengthChanged?.Invoke(this, e); + } + + /// + /// Gets the byte collection. + /// + public ByteCollection Bytes { get; } + + #region IByteProvider Members + /// + /// True, when changes are done. + /// + public bool HasChanges() + { + return _hasChanges; + } + + /// + /// Applies changes. + /// + public void ApplyChanges() + { + _hasChanges = false; + + _modifiedStream.Position = 0; + _modifiedStream.Write(Bytes.ToArray()); + } + + /// + /// Occurs, when the write buffer contains new changes. + /// + public event EventHandler Changed; + + /// + /// Occurs, when InsertBytes or DeleteBytes method is called. + /// + public event EventHandler LengthChanged; + + /// + /// Reads a byte from the byte collection. + /// + /// the index of the byte to read + /// the byte + public byte ReadByte(long index) + { return Bytes[(int)index]; } + + /// + /// Write a byte into the byte collection. + /// + /// the index of the byte to write. + /// the byte + public void WriteByte(long index, byte value) + { + Bytes[(int)index] = value; + OnChanged(EventArgs.Empty); + } + + /// + /// Deletes bytes from the byte collection. + /// + /// the start index of the bytes to delete. + /// the length of bytes to delete. + public void DeleteBytes(long index, long length) + { + int internal_index = (int)Math.Max(0, index); + int internal_length = (int)Math.Min((int)Length, length); + Bytes.RemoveRange(internal_index, internal_length); + + OnLengthChanged(EventArgs.Empty); + OnChanged(EventArgs.Empty); + } + + /// + /// Inserts byte into the byte collection. + /// + /// the start index of the bytes in the byte collection + /// the byte array to insert + public void InsertBytes(long index, byte[] bs) + { + Bytes.InsertRange((int)index, bs); + + OnLengthChanged(EventArgs.Empty); + OnChanged(EventArgs.Empty); + } + + /// + /// Gets the length of the bytes in the byte collection. + /// + public long Length => Bytes.Count; + + /// + /// Returns true + /// + public bool SupportsWriteByte() + { + return true; + } + + /// + /// Returns true + /// + public bool SupportsInsertBytes() + { + return true; + } + + /// + /// Returns true + /// + public bool SupportsDeleteBytes() + { + return true; + } + #endregion +} diff --git a/StructuredStorageExplorer/StructuredStorageExplorer.csproj b/StructuredStorageExplorer/StructuredStorageExplorer.csproj new file mode 100644 index 00000000..d5002c46 --- /dev/null +++ b/StructuredStorageExplorer/StructuredStorageExplorer.csproj @@ -0,0 +1,18 @@ + + + net8.0-windows + WinExe + true + 12.0 + enable + enable + + + + + + + + + + \ No newline at end of file diff --git a/StructuredStorageExplorer/Utils.cs b/StructuredStorageExplorer/Utils.cs new file mode 100644 index 00000000..5ecc3b14 --- /dev/null +++ b/StructuredStorageExplorer/Utils.cs @@ -0,0 +1,46 @@ +namespace StructuredStorageExplorer; + +class Utils +{ + public static DialogResult InputBox(string title, string promptText, ref string value) + { + Form form = new Form(); + Label label = new Label(); + TextBox textBox = new TextBox(); + Button buttonOk = new Button(); + Button buttonCancel = new Button(); + + form.Text = title; + label.Text = promptText; + textBox.Text = value; + + buttonOk.Text = "OK"; + buttonCancel.Text = "Cancel"; + buttonOk.DialogResult = DialogResult.OK; + buttonCancel.DialogResult = DialogResult.Cancel; + + label.SetBounds(9, 20, 372, 13); + textBox.SetBounds(12, 36, 372, 20); + buttonOk.SetBounds(228, 72, 75, 23); + buttonCancel.SetBounds(309, 72, 75, 23); + + label.AutoSize = true; + textBox.Anchor = textBox.Anchor | AnchorStyles.Right; + buttonOk.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + buttonCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + + form.ClientSize = new Size(396, 107); + form.Controls.AddRange([label, textBox, buttonOk, buttonCancel]); + form.ClientSize = new Size(Math.Max(300, label.Right + 10), form.ClientSize.Height); + form.FormBorderStyle = FormBorderStyle.FixedDialog; + form.StartPosition = FormStartPosition.CenterScreen; + form.MinimizeBox = false; + form.MaximizeBox = false; + form.AcceptButton = buttonOk; + form.CancelButton = buttonCancel; + + DialogResult dialogResult = form.ShowDialog(); + value = textBox.Text; + return dialogResult; + } +} diff --git a/StructuredStorageExplorer/app.config b/StructuredStorageExplorer/app.config new file mode 100644 index 00000000..6cd5da2a --- /dev/null +++ b/StructuredStorageExplorer/app.config @@ -0,0 +1,18 @@ + + + + +
+ + + + + + True + + + False + + + + diff --git a/StructuredStorageExplorer/img/disk.png b/StructuredStorageExplorer/img/disk.png new file mode 100644 index 0000000000000000000000000000000000000000..99d532e8b1750115952f97302a92d713c0486f97 GIT binary patch literal 620 zcmV-y0+aoTP)~H+MJzd|s z^YP1Hc07G_>)Lgir!F1{Qn4GcTg%?koHo<=1qRN{}nPDolOeI^o4N5I>! zU$N=L=sg~ zDx#dOA*B0N~cqPsWI(^rbbkh)DS0_H_UN0C4l_kvWIm2#Kyy6%BCh z(yIUf003&1xdx>t$*eR2ZvXxT0001Z_R$y3Iju92q*wg58};}zm(OaAH=p|y0002M zh5O5#fxp|~jc?yi@+7$`d4Q6Hl%z;WiWG??NXR{Hx%)pMd~SE0000OQI literal 0 HcmV?d00001 diff --git a/StructuredStorageExplorer/img/door_out.png b/StructuredStorageExplorer/img/door_out.png new file mode 100644 index 0000000000000000000000000000000000000000..2541d2bcbc218b194f79fd99f67d33de1873c6c4 GIT binary patch literal 688 zcmV;h0#E&kP)GS2eE@_I zS~TaE^z1tT1me$mOd>fuB1*9ukjYHe@2!~sjG5tP)N7xSr3G9P+3oKa++V)SLaGru zn`QvjgqvWRa7{oUyOUDA37GDE%9f3r>9Muk`Z$59p<W>iYj7vxikNWw_8sK+%_fnobvCa5%KNOO6e%CReDLPLmVwdHp%H5J z8cVW-n2=oPDz8D+5J{LSmLlCXMPg`l3MX6KVJNMw&n~!g&9zA<=CHFVyLz1l^k+DTiboyDIuKD~MJE&R)Oo;bO6gPHCylVLL*a<{&sTsdkI7k&ZU WO{4dBd(FK70000x(K@^6+>g^d@v4;gkbWsEoXE%32*i1tcpTNXd5CcIl)ECgqz|2rE6EW}s7R?kl za1q`0GCkMruC6-2LANtwVlsgzsp4?{@7$`KBv!G66>Vie3h?3OmEEkjwdLG0PgLVi z`!N((f$A@n17Ldj#`};0I3@iHJ5M{#IZz|UIYRm4(!uV7eYIYIwQf&}_2J~}>pQ^n z6o8--^T(=hkBNQ_k{-_GWE;FMW7!p}f{NG3nHZ{D5<3d8&tLh%a4AqqnjMkr3m&fkMdECD3N5}Unig5wy40;>lo4j~k+e}v)` zR6)J8Mk*u=SpB`p6o)7j?S0T@9?bz#m@l>gc*zk__|*!FMcHwP!gwLJvS~9c0px8E zWC#5QQ<|d}62BjvZR2H60wE-&H;pyTSqH(@-Vl>|&1p(LP>kg~E zYiz5X^`c$+%8#zC{u)yfe-5 zmgid={Z3k(ERKCKrE7DF;=x4^O+ pzO8rLO8p|Ip=x)jHOtWj`bJBmKdh_V<`47(gQu&X%Q~loCIFbEay|e6 literal 0 HcmV?d00001 diff --git a/StructuredStorageExplorer/img/storage.png b/StructuredStorageExplorer/img/storage.png new file mode 100644 index 0000000000000000000000000000000000000000..cbde61822a99090c03c45759518ad41a562e6bca GIT binary patch literal 396 zcmV;70dxL|P)!;NE`7)<>#@EXE+d`cov zvkwEWuoQ;m=eHn3w&2r$faUMszYJV_!VG_Z|HP05i5q#upTaRKY#u({Q32r zfgAwR%kum8Z-(DLup99C-7AJ0XZGSX1nh#}KfhsWe*c=`=D9-*c0uVlHD5Wl1Ly*v z0mpZ)#Hzp{D3#&E%ZE4(c=_Zm1B-x|41=zP3swd1p5DP920-t#ym)w!xji7{tEfEm|Kzbnt{P|730ssE}J8;R$-c>er3NHbV1vLP(!2H*mf zo{~UP63m`@^eY2~0e}AdVfg!xf#KTY_h7?*|NhPJ>(?(h2Ju1SKK907wb%^+xvVuwh7`}v3A^Y>p2AHV)$`26h`SS?H+K!5=N-dA4Q?faNt00000NkvXXu0mjf DU{|iw literal 0 HcmV?d00001 From 753071b77fa750c832059bb75452d36a91b61828 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 11:07:42 +1300 Subject: [PATCH 097/114] Refactor OLE extension --- OpenMcdf.Ole/Common.cs | 8 - OpenMcdf.Ole/DefaultPropertyFactory.cs | 2 +- OpenMcdf.Ole/DictionaryEntry.cs | 44 +++-- OpenMcdf.Ole/DictionaryProperty.cs | 20 +-- .../DocumentSummaryInfoPropertyFactory.cs | 6 +- ...matIdentifiers.cs => FormatIdentifiers.cs} | 2 +- OpenMcdf.Ole/IDictionaryProperty.cs | 5 - OpenMcdf.Ole/IProperty.cs | 11 +- OpenMcdf.Ole/ITypedPropertyValue.cs | 23 ++- OpenMcdf.Ole/Identifiers.cs | 27 --- OpenMcdf.Ole/OlePropertiesContainer.cs | 145 ++++------------ OpenMcdf.Ole/OleProperty.cs | 34 +--- OpenMcdf.Ole/PropertyFactory.cs | 156 ++++++------------ ...yIdentifiers.cs => PropertyIdentifiers.cs} | 30 +++- OpenMcdf.Ole/PropertySet.cs | 27 +-- OpenMcdf.Ole/PropertySetStream.cs | 105 ++++++------ OpenMcdf.Ole/TypedPropertyValue.cs | 18 +- OpenMcdf.Ole/VTPropertyType.cs | 2 + 18 files changed, 242 insertions(+), 423 deletions(-) delete mode 100644 OpenMcdf.Ole/Common.cs rename OpenMcdf.Ole/{WellKnownFormatIdentifiers.cs => FormatIdentifiers.cs} (92%) delete mode 100644 OpenMcdf.Ole/IDictionaryProperty.cs delete mode 100644 OpenMcdf.Ole/Identifiers.cs rename OpenMcdf.Ole/{ProperyIdentifiers.cs => PropertyIdentifiers.cs} (62%) diff --git a/OpenMcdf.Ole/Common.cs b/OpenMcdf.Ole/Common.cs deleted file mode 100644 index 3ee12e48..00000000 --- a/OpenMcdf.Ole/Common.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OpenMcdf.Ole; - -public enum PropertyDimensions -{ - IsScalar, - IsVector, - IsArray -} diff --git a/OpenMcdf.Ole/DefaultPropertyFactory.cs b/OpenMcdf.Ole/DefaultPropertyFactory.cs index 4b432a7e..7d784623 100644 --- a/OpenMcdf.Ole/DefaultPropertyFactory.cs +++ b/OpenMcdf.Ole/DefaultPropertyFactory.cs @@ -3,5 +3,5 @@ // The default property factory. internal sealed class DefaultPropertyFactory : PropertyFactory { - public static PropertyFactory Instance { get; } = new DefaultPropertyFactory(); + public static DefaultPropertyFactory Default { get; } = new(); } diff --git a/OpenMcdf.Ole/DictionaryEntry.cs b/OpenMcdf.Ole/DictionaryEntry.cs index 68c04b6e..9fa05739 100644 --- a/OpenMcdf.Ole/DictionaryEntry.cs +++ b/OpenMcdf.Ole/DictionaryEntry.cs @@ -5,6 +5,7 @@ namespace OpenMcdf.Ole; public class DictionaryEntry { readonly int codePage; + private byte[]? nameBytes; public DictionaryEntry(int codePage) { @@ -12,21 +13,28 @@ public DictionaryEntry(int codePage) } public uint PropertyIdentifier { get; set; } + public int Length { get; set; } - public string Name => GetName(); - private byte[] nameBytes; + public string Name + { + get + { + if (nameBytes is null) + return string.Empty; + + string result = Encoding.GetEncoding(codePage).GetString(nameBytes); + result = result.Trim('\0'); + return result; + } + } public void Read(BinaryReader br) { PropertyIdentifier = br.ReadUInt32(); Length = br.ReadInt32(); - if (codePage != CodePages.WinUnicode) - { - nameBytes = br.ReadBytes(Length); - } - else + if (codePage == CodePages.WinUnicode) { nameBytes = br.ReadBytes(Length << 1); @@ -34,28 +42,16 @@ public void Read(BinaryReader br) if (m > 0) br.ReadBytes(m); } + else + { + nameBytes = br.ReadBytes(Length); + } } public void Write(BinaryWriter bw) { bw.Write(PropertyIdentifier); bw.Write(Length); - bw.Write(nameBytes); - - //if (codePage == CP_WINUNICODE) - // int m = Length % 4; - - //if (m > 0) - // for (int i = 0; i < m; i++) - // bw.Write((byte)m); - } - - private string GetName() - { - string result = Encoding.GetEncoding(codePage).GetString(nameBytes); - - result = result.Trim('\0'); - - return result; + bw.Write(nameBytes!); } } diff --git a/OpenMcdf.Ole/DictionaryProperty.cs b/OpenMcdf.Ole/DictionaryProperty.cs index 1089307c..7c8a0759 100644 --- a/OpenMcdf.Ole/DictionaryProperty.cs +++ b/OpenMcdf.Ole/DictionaryProperty.cs @@ -2,24 +2,22 @@ namespace OpenMcdf.Ole; -public class DictionaryProperty : IDictionaryProperty +public class DictionaryProperty : IProperty { private readonly int codePage; + private Dictionary? entries = new(); public DictionaryProperty(int codePage) { this.codePage = codePage; - entries = new Dictionary(); } public PropertyType PropertyType => PropertyType.DictionaryProperty; - private Dictionary entries; - - public object Value + public object? Value { get => entries; - set => entries = (Dictionary)value; + set => entries = (Dictionary?)value; } public void Read(BinaryReader br) @@ -30,10 +28,10 @@ public void Read(BinaryReader br) for (uint i = 0; i < numEntries; i++) { - DictionaryEntry de = new DictionaryEntry(codePage); + DictionaryEntry de = new(codePage); de.Read(br); - entries.Add(de.PropertyIdentifier, de.Name); + entries!.Add(de.PropertyIdentifier, de.Name); } int m = (int)(br.BaseStream.Position - curPos) % 4; @@ -58,7 +56,7 @@ public void Write(BinaryWriter bw) { long curPos = bw.BaseStream.Position; - bw.Write(entries.Count); + bw.Write(entries!.Count); foreach (KeyValuePair kv in entries) { @@ -75,7 +73,7 @@ private void WriteEntry(BinaryWriter bw, uint propertyIdentifier, string name) // Write the PropertyIdentifier bw.Write(propertyIdentifier); - // Encode string data with the current codepage + // Encode string data with the current code page byte[] nameBytes = Encoding.GetEncoding(codePage).GetBytes(name); uint byteLength = (uint)nameBytes.Length; @@ -118,7 +116,7 @@ private void WriteEntry(BinaryWriter bw, uint propertyIdentifier, string name) } // Write as much padding as needed to pad fieldLength to a multiple of 4 bytes - private void WritePaddingIfNeeded(BinaryWriter bw, int fieldLength) + private static void WritePaddingIfNeeded(BinaryWriter bw, int fieldLength) { int m = fieldLength % 4; diff --git a/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs index 18602bd8..551e55ed 100644 --- a/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs +++ b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs @@ -3,14 +3,14 @@ // A separate factory for DocumentSummaryInformation properties, to handle special cases with unaligned strings. internal sealed class DocumentSummaryInfoPropertyFactory : PropertyFactory { - public static PropertyFactory Instance { get; } = new DocumentSummaryInfoPropertyFactory(); + public static DocumentSummaryInfoPropertyFactory Default { get; } = new(); protected override ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) { // PIDDSI_HEADINGPAIR and PIDDSI_DOCPARTS use unaligned (unpadded) strings - the others are padded - if (propertyIdentifier == 0x0000000C || propertyIdentifier == 0x0000000D) + if (propertyIdentifier is 0x0000000C or 0x0000000D) return new VT_Unaligned_LPSTR_Property(vType, codePage, isVariant); - return base.CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); + return CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); } } diff --git a/OpenMcdf.Ole/WellKnownFormatIdentifiers.cs b/OpenMcdf.Ole/FormatIdentifiers.cs similarity index 92% rename from OpenMcdf.Ole/WellKnownFormatIdentifiers.cs rename to OpenMcdf.Ole/FormatIdentifiers.cs index 9161fdbe..9fee7a2d 100644 --- a/OpenMcdf.Ole/WellKnownFormatIdentifiers.cs +++ b/OpenMcdf.Ole/FormatIdentifiers.cs @@ -1,6 +1,6 @@ namespace OpenMcdf.Ole; -public static class WellKnownFormatIdentifiers +public static class FormatIdentifiers { public static readonly Guid SummaryInformation = new("{F29F85E0-4FF9-1068-AB91-08002B27B3D9}"); public static readonly Guid DocSummaryInformation = new("{D5CDD502-2E9C-101B-9397-08002B2CF9AE}"); diff --git a/OpenMcdf.Ole/IDictionaryProperty.cs b/OpenMcdf.Ole/IDictionaryProperty.cs deleted file mode 100644 index 8b20b053..00000000 --- a/OpenMcdf.Ole/IDictionaryProperty.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace OpenMcdf.Ole; - -public interface IDictionaryProperty : IProperty -{ -} \ No newline at end of file diff --git a/OpenMcdf.Ole/IProperty.cs b/OpenMcdf.Ole/IProperty.cs index 9d44eed4..6e9e2610 100644 --- a/OpenMcdf.Ole/IProperty.cs +++ b/OpenMcdf.Ole/IProperty.cs @@ -8,14 +8,7 @@ public enum PropertyType public interface IProperty : IBinarySerializable { - object Value - { - get; - set; - } + object? Value { get; set; } - PropertyType PropertyType - { - get; - } + PropertyType PropertyType { get; } } diff --git a/OpenMcdf.Ole/ITypedPropertyValue.cs b/OpenMcdf.Ole/ITypedPropertyValue.cs index 76198997..94c4c5a3 100644 --- a/OpenMcdf.Ole/ITypedPropertyValue.cs +++ b/OpenMcdf.Ole/ITypedPropertyValue.cs @@ -1,20 +1,17 @@ namespace OpenMcdf.Ole; +public enum PropertyDimensions +{ + IsScalar, + IsVector, + IsArray +} + public interface ITypedPropertyValue : IProperty { - VTPropertyType VTType - { - get; - //set; - } + VTPropertyType VTType { get; } - PropertyDimensions PropertyDimensions - { - get; - } + PropertyDimensions PropertyDimensions { get; } - bool IsVariant - { - get; - } + bool IsVariant { get; } } diff --git a/OpenMcdf.Ole/Identifiers.cs b/OpenMcdf.Ole/Identifiers.cs deleted file mode 100644 index 860297d0..00000000 --- a/OpenMcdf.Ole/Identifiers.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace OpenMcdf.Ole; - -public static class Identifiers -{ - public static string GetDescription(uint identifier, ContainerType map, IDictionary customDict = null) - { - IDictionary nameDictionary = customDict; - - if (nameDictionary is null) - { - switch (map) - { - case ContainerType.SummaryInfo: - nameDictionary = PropertyIdentifiers.SummaryInfo; - break; - case ContainerType.DocumentSummaryInfo: - nameDictionary = PropertyIdentifiers.DocumentSummaryInfo; - break; - } - } - - if (nameDictionary?.TryGetValue(identifier, out string? value) == true) - return value; - - return $"0x{identifier:x8}"; - } -} diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index 9191cd70..8ff3f80f 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -15,11 +15,9 @@ public enum ContainerType public class OlePropertiesContainer { - public Dictionary PropertyNames; + public Dictionary? PropertyNames { get; private set; } - public OlePropertiesContainer UserDefinedProperties { get; private set; } - - public bool HasUserDefinedProperties { get; private set; } + public OlePropertiesContainer? UserDefinedProperties { get; private set; } public ContainerType ContainerType { get; } private Guid? FmtID0 { get; } @@ -27,53 +25,7 @@ public class OlePropertiesContainer public PropertyContext Context { get; } private readonly List properties = new(); - internal Stream cfStream; - - /* - Property name Property ID PID Type - Codepage PID_CODEPAGE 1 VT_I2 - Title PID_TITLE 2 VT_LPSTR - Subject PID_SUBJECT 3 VT_LPSTR - Author PID_AUTHOR 4 VT_LPSTR - Keywords PID_KEYWORDS 5 VT_LPSTR - Comments PID_COMMENTS 6 VT_LPSTR - Template PID_TEMPLATE 7 VT_LPSTR - Last Saved By PID_LASTAUTHOR 8 VT_LPSTR - Revision Number PID_REVNUMBER 9 VT_LPSTR - Last Printed PID_LASTPRINTED 11 VT_FILETIME - Create Time/Date PID_CREATE_DTM 12 VT_FILETIME - Last Save Time/Date PID_LASTSAVE_DTM 13 VT_FILETIME - Page Count PID_PAGECOUNT 14 VT_I4 - Word Count PID_WORDCOUNT 15 VT_I4 - Character Count PID_CHARCOUNT 16 VT_I4 - Creating Application PID_APPNAME 18 VT_LPSTR - Security PID_SECURITY 19 VT_I4 - */ - public class SummaryInfoProperties - { - public short CodePage { get; set; } - public string Title { get; set; } - public string Subject { get; set; } - public string Author { get; set; } - public string KeyWords { get; set; } - public string Comments { get; set; } - public string Template { get; set; } - public string LastSavedBy { get; set; } - public string RevisionNumber { get; set; } - public DateTime LastPrinted { get; set; } - public DateTime CreateTime { get; set; } - public DateTime LastSavedTime { get; set; } - public int PageCount { get; set; } - public int WordCount { get; set; } - public int CharacterCount { get; set; } - public string CreatingApplication { get; set; } - public int Security { get; set; } - } - - public static OlePropertiesContainer CreateNewSummaryInfo(SummaryInfoProperties sumInfoProps) - { - return null; - } + internal Stream? cfStream; public OlePropertiesContainer(int codePage, ContainerType containerType) { @@ -92,18 +44,19 @@ public OlePropertiesContainer(CfbStream cfStream) this.cfStream = cfStream; + cfStream.Position = 0; using BinaryReader reader = new(cfStream); pStream.Read(reader); - if (pStream.FMTID0 == WellKnownFormatIdentifiers.SummaryInformation) + if (pStream.FMTID0 == FormatIdentifiers.SummaryInformation) ContainerType = ContainerType.SummaryInfo; - else if (pStream.FMTID0 == WellKnownFormatIdentifiers.DocSummaryInformation) + else if (pStream.FMTID0 == FormatIdentifiers.DocSummaryInformation) ContainerType = ContainerType.DocumentSummaryInfo; else ContainerType = ContainerType.AppSpecific; FmtID0 = pStream.FMTID0; - PropertyNames = (Dictionary?)pStream.PropertySet0.Properties + PropertyNames = (Dictionary?)pStream.PropertySet0!.Properties .FirstOrDefault(p => p.PropertyType == PropertyType.DictionaryProperty)?.Value; Context = new PropertyContext() @@ -113,17 +66,17 @@ public OlePropertiesContainer(CfbStream cfStream) for (int i = 0; i < pStream.PropertySet0.Properties.Count; i++) { - if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 0) continue; - //if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 1) continue; - //if (pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier == 0x80000000) continue; + PropertyIdentifierAndOffset propertyIdentifierAndOffset = pStream.PropertySet0.PropertyIdentifierAndOffsets[i]; + if (propertyIdentifierAndOffset.PropertyIdentifier == 0) continue; + //if (propertyIdentifierAndOffset.PropertyIdentifier == 1) continue; + //if (propertyIdentifierAndOffset.PropertyIdentifier == 0x80000000) continue; var p = (ITypedPropertyValue)pStream.PropertySet0.Properties[i]; - PropertyIdentifierAndOffset poi = pStream.PropertySet0.PropertyIdentifierAndOffsets[i]; - var op = new OleProperty(this) + OleProperty op = new(this) { VTType = p.VTType, - PropertyIdentifier = pStream.PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, + PropertyIdentifier = propertyIdentifierAndOffset.PropertyIdentifier, Value = p.Value }; @@ -132,28 +85,28 @@ public OlePropertiesContainer(CfbStream cfStream) if (pStream.NumPropertySets == 2) { - UserDefinedProperties = new OlePropertiesContainer(pStream.PropertySet1.PropertyContext.CodePage, ContainerType.UserDefinedProperties); - HasUserDefinedProperties = true; + PropertySet propertySet1 = pStream.PropertySet1!; + UserDefinedProperties = new OlePropertiesContainer(propertySet1.PropertyContext.CodePage, ContainerType.UserDefinedProperties); - for (int i = 0; i < pStream.PropertySet1.Properties.Count; i++) + for (int i = 0; i < propertySet1.Properties.Count; i++) { - if (pStream.PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier is 0 or 0x80000000) + PropertyIdentifierAndOffset propertyIdentifierAndOffset = propertySet1.PropertyIdentifierAndOffsets[i]; + if (propertyIdentifierAndOffset.PropertyIdentifier is 0 or 0x80000000) continue; - var p = (ITypedPropertyValue)pStream.PropertySet1.Properties[i]; - PropertyIdentifierAndOffset poi = pStream.PropertySet1.PropertyIdentifierAndOffsets[i]; + var p = (ITypedPropertyValue)propertySet1.Properties[i]; OleProperty op = new(UserDefinedProperties) { VTType = p.VTType, - PropertyIdentifier = pStream.PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, + PropertyIdentifier = propertyIdentifierAndOffset.PropertyIdentifier, Value = p.Value }; UserDefinedProperties.properties.Add(op); } - var existingPropertyNames = (Dictionary?)pStream.PropertySet1.Properties + var existingPropertyNames = (Dictionary?)propertySet1.Properties .FirstOrDefault(p => p.PropertyType == PropertyType.DictionaryProperty)?.Value; UserDefinedProperties.PropertyNames = existingPropertyNames ?? new Dictionary(); @@ -162,10 +115,9 @@ public OlePropertiesContainer(CfbStream cfStream) public IList Properties => properties; - public OleProperty NewProperty(VTPropertyType vtPropertyType, uint propertyIdentifier, string? propertyName = null) + public OleProperty CreateProperty(VTPropertyType vtPropertyType, uint propertyIdentifier, string? propertyName = null) { - //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); - var op = new OleProperty(this) + OleProperty op = new(this) { VTType = vtPropertyType, PropertyIdentifier = propertyIdentifier @@ -174,18 +126,13 @@ public OleProperty NewProperty(VTPropertyType vtPropertyType, uint propertyIdent return op; } - public void AddProperty(OleProperty property) - { - //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); - properties.Add(property); - } + public void Add(OleProperty property) => properties.Add(property); public void RemoveProperty(uint propertyIdentifier) { - //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); OleProperty? toRemove = properties.FirstOrDefault(o => o.PropertyIdentifier == propertyIdentifier); - if (toRemove != null) + if (toRemove is not null) properties.Remove(toRemove); } @@ -204,7 +151,7 @@ public OlePropertiesContainer CreateUserDefinedProperties(int codePage) if (ContainerType != ContainerType.DocumentSummaryInfo) throw new InvalidOperationException($"Only a DocumentSummaryInfo can contain user defined properties. Current container type is {ContainerType}"); - // Create the container, and add the codepage to the initial set of properties + // Create the container, and add the code page to the initial set of properties UserDefinedProperties = new OlePropertiesContainer(codePage, ContainerType.UserDefinedProperties) { PropertyNames = new Dictionary() @@ -218,19 +165,15 @@ public OlePropertiesContainer CreateUserDefinedProperties(int codePage) }; UserDefinedProperties.properties.Add(op); - HasUserDefinedProperties = true; return UserDefinedProperties; } public void Save(Stream cfStream) { - //throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0"); - //properties.Sort((a, b) => a.PropertyIdentifier.CompareTo(b.PropertyIdentifier)); - using BinaryWriter bw = new(cfStream); - Guid fmtId0 = FmtID0 ?? (ContainerType == ContainerType.SummaryInfo ? WellKnownFormatIdentifiers.SummaryInformation : WellKnownFormatIdentifiers.DocSummaryInformation); + Guid fmtId0 = FmtID0 ?? (ContainerType == ContainerType.SummaryInfo ? FormatIdentifiers.SummaryInformation : FormatIdentifiers.DocSummaryInformation); PropertySetStream ps = new() { @@ -249,54 +192,46 @@ public void Save(Stream cfStream) PropertySet0 = new PropertySet { - NumProperties = (uint)Properties.Count, - PropertyIdentifierAndOffsets = new List(), - Properties = new List(), PropertyContext = Context } }; // If we're writing an AppSpecific property set and have property names, then add a dictionary property - if (ContainerType == ContainerType.AppSpecific && PropertyNames != null && PropertyNames.Count > 0) + if (ContainerType == ContainerType.AppSpecific && PropertyNames is not null && PropertyNames.Count > 0) { - AddDictionaryPropertyToPropertySet(PropertyNames, ps.PropertySet0); - ps.PropertySet0.NumProperties += 1; + ps.PropertySet0.Add(PropertyNames); } PropertyFactory factory = - ContainerType == ContainerType.DocumentSummaryInfo ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; + ContainerType == ContainerType.DocumentSummaryInfo ? DocumentSummaryInfoPropertyFactory.Default : DefaultPropertyFactory.Default; foreach (OleProperty op in Properties) { - ITypedPropertyValue p = factory.NewProperty(op.VTType, Context.CodePage, op.PropertyIdentifier); + ITypedPropertyValue p = factory.CreateProperty(op.VTType, Context.CodePage, op.PropertyIdentifier); p.Value = op.Value; ps.PropertySet0.Properties.Add(p); ps.PropertySet0.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); } - if (HasUserDefinedProperties) + if (UserDefinedProperties is not null) { ps.NumPropertySets = 2; ps.PropertySet1 = new PropertySet { - // Number of user defined properties, plus 1 for the name dictionary - NumProperties = (uint)UserDefinedProperties.Properties.Count + 1, - PropertyIdentifierAndOffsets = new List(), - Properties = new List(), PropertyContext = UserDefinedProperties.Context }; - ps.FMTID1 = WellKnownFormatIdentifiers.UserDefinedProperties; + ps.FMTID1 = FormatIdentifiers.UserDefinedProperties; ps.Offset1 = 0; // Add the dictionary containing the property names - AddDictionaryPropertyToPropertySet(UserDefinedProperties.PropertyNames, ps.PropertySet1); + ps.PropertySet1.Add(UserDefinedProperties.PropertyNames!); // Add the properties themselves foreach (OleProperty op in UserDefinedProperties.Properties) { - ITypedPropertyValue p = DefaultPropertyFactory.Instance.NewProperty(op.VTType, ps.PropertySet1.PropertyContext.CodePage, op.PropertyIdentifier); + ITypedPropertyValue p = DefaultPropertyFactory.Default.CreateProperty(op.VTType, ps.PropertySet1.PropertyContext.CodePage, op.PropertyIdentifier); p.Value = op.Value; ps.PropertySet1.Properties.Add(p); ps.PropertySet1.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); @@ -305,14 +240,4 @@ public void Save(Stream cfStream) ps.Write(bw); } - - private void AddDictionaryPropertyToPropertySet(Dictionary propertyNames, PropertySet propertySet) - { - IDictionaryProperty dictionaryProperty = new DictionaryProperty(propertySet.PropertyContext.CodePage) - { - Value = propertyNames - }; - propertySet.Properties.Add(dictionaryProperty); - propertySet.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = 0, Offset = 0 }); - } } diff --git a/OpenMcdf.Ole/OleProperty.cs b/OpenMcdf.Ole/OleProperty.cs index 6d8cefbf..67ab4bd5 100644 --- a/OpenMcdf.Ole/OleProperty.cs +++ b/OpenMcdf.Ole/OleProperty.cs @@ -1,33 +1,22 @@ namespace OpenMcdf.Ole; -public class OleProperty +public sealed class OleProperty { private readonly OlePropertiesContainer container; + object? value; internal OleProperty(OlePropertiesContainer container) { this.container = container; } - public string PropertyName => DecodePropertyIdentifier(); + public string PropertyName => PropertyIdentifiers.GetDescription(PropertyIdentifier, container.ContainerType, container.PropertyNames); - private string DecodePropertyIdentifier() - { - return Identifiers.GetDescription(PropertyIdentifier, container.ContainerType, container.PropertyNames); - } - - //public string Description { get { return description; } public uint PropertyIdentifier { get; internal set; } - public VTPropertyType VTType - { - get; - internal set; - } - - object value; + public VTPropertyType VTType { get; internal set; } - public object Value + public object? Value { get { @@ -47,18 +36,9 @@ public object Value set => this.value = value; } - public override bool Equals(object obj) - { - if (obj is not OleProperty other) - return false; - - return other.PropertyIdentifier == PropertyIdentifier; - } + public override bool Equals(object? obj) => obj is OleProperty other && other.PropertyIdentifier == PropertyIdentifier; - public override int GetHashCode() - { - return (int)PropertyIdentifier; - } + public override int GetHashCode() => (int)PropertyIdentifier; public override string ToString() => $"{PropertyName} - {VTType} - {Value}"; } diff --git a/OpenMcdf.Ole/PropertyFactory.cs b/OpenMcdf.Ole/PropertyFactory.cs index c2c201ac..968e7f30 100644 --- a/OpenMcdf.Ole/PropertyFactory.cs +++ b/OpenMcdf.Ole/PropertyFactory.cs @@ -11,7 +11,7 @@ static PropertyFactory() #endif } - public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant = false) + public ITypedPropertyValue CreateProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant = false) { ITypedPropertyValue pr = (VTPropertyType)((ushort)vType & 0x00FF) switch { @@ -39,35 +39,29 @@ public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, uint VTPropertyType.VT_CF => new VT_CF_Property(vType, isVariant), VTPropertyType.VT_BLOB_OBJECT or VTPropertyType.VT_BLOB => new VT_BLOB_Property(vType, isVariant), VTPropertyType.VT_CLSID => new VT_CLSID_Property(vType, isVariant), - _ => throw new Exception("Unrecognized property type"), + _ => throw new ArgumentException("Unrecognized property type"), }; return pr; } - protected virtual ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) - { - return new VT_LPSTR_Property(vType, codePage, isVariant); - } + protected virtual ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) => new VT_LPSTR_Property(vType, codePage, isVariant); #region Property implementations - private class VT_EMPTY_Property : TypedPropertyValue + private sealed class VT_EMPTY_Property : TypedPropertyValue { public VT_EMPTY_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { } - public override object ReadScalarValue(BinaryReader br) - { - return null; - } + public override object? ReadScalarValue(BinaryReader br) => null; public override void WriteScalarValue(BinaryWriter bw, object pValue) { } } - private class VT_I1_Property : TypedPropertyValue + private sealed class VT_I1_Property : TypedPropertyValue { public VT_I1_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -79,13 +73,10 @@ public override sbyte ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, sbyte pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, sbyte pValue) => bw.Write(pValue); } - private class VT_UI1_Property : TypedPropertyValue + private sealed class VT_UI1_Property : TypedPropertyValue { public VT_UI1_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -97,13 +88,10 @@ public override byte ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, byte pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, byte pValue) => bw.Write(pValue); } - private class VT_UI4_Property : TypedPropertyValue + private sealed class VT_UI4_Property : TypedPropertyValue { public VT_UI4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -115,13 +103,10 @@ public override uint ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, uint pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, uint pValue) => bw.Write(pValue); } - private class VT_UI8_Property : TypedPropertyValue + private sealed class VT_UI8_Property : TypedPropertyValue { public VT_UI8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -133,13 +118,10 @@ public override ulong ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, ulong pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, ulong pValue) => bw.Write(pValue); } - private class VT_I2_Property : TypedPropertyValue + private sealed class VT_I2_Property : TypedPropertyValue { public VT_I2_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -151,13 +133,10 @@ public override short ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, short pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, short pValue) => bw.Write(pValue); } - private class VT_UI2_Property : TypedPropertyValue + private sealed class VT_UI2_Property : TypedPropertyValue { public VT_UI2_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -169,13 +148,10 @@ public override ushort ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, ushort pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, ushort pValue) => bw.Write(pValue); } - private class VT_I4_Property : TypedPropertyValue + private sealed class VT_I4_Property : TypedPropertyValue { public VT_I4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -187,13 +163,10 @@ public override int ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, int pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, int pValue) => bw.Write(pValue); } - private class VT_I8_Property : TypedPropertyValue + private sealed class VT_I8_Property : TypedPropertyValue { public VT_I8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -205,13 +178,10 @@ public override long ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, long pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, long pValue) => bw.Write(pValue); } - private class VT_INT_Property : TypedPropertyValue + private sealed class VT_INT_Property : TypedPropertyValue { public VT_INT_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -223,13 +193,10 @@ public override int ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, int pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, int pValue) => bw.Write(pValue); } - private class VT_UINT_Property : TypedPropertyValue + private sealed class VT_UINT_Property : TypedPropertyValue { public VT_UINT_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -241,13 +208,10 @@ public override uint ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, uint pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, uint pValue) => bw.Write(pValue); } - private class VT_R4_Property : TypedPropertyValue + private sealed class VT_R4_Property : TypedPropertyValue { public VT_R4_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -259,13 +223,10 @@ public override float ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, float pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, float pValue) => bw.Write(pValue); } - private class VT_R8_Property : TypedPropertyValue + private sealed class VT_R8_Property : TypedPropertyValue { public VT_R8_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -277,13 +238,10 @@ public override double ReadScalarValue(BinaryReader br) return r; } - public override void WriteScalarValue(BinaryWriter bw, double pValue) - { - bw.Write(pValue); - } + public override void WriteScalarValue(BinaryWriter bw, double pValue) => bw.Write(pValue); } - private class VT_CY_Property : TypedPropertyValue + private sealed class VT_CY_Property : TypedPropertyValue { public VT_CY_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -298,13 +256,10 @@ public override long ReadScalarValue(BinaryReader br) return tmp; } - public override void WriteScalarValue(BinaryWriter bw, long pValue) - { - bw.Write(pValue * 10000); - } + public override void WriteScalarValue(BinaryWriter bw, long pValue) => bw.Write(pValue * 10000); } - private class VT_DATE_Property : TypedPropertyValue + private sealed class VT_DATE_Property : TypedPropertyValue { public VT_DATE_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -317,15 +272,11 @@ public override DateTime ReadScalarValue(BinaryReader br) return DateTime.FromOADate(temp); } - public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) - { - bw.Write(pValue.ToOADate()); - } + public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) => bw.Write(pValue.ToOADate()); } protected class VT_LPSTR_Property : TypedPropertyValue { - private byte[] data; private readonly int codePage; public VT_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, isVariant) @@ -336,7 +287,7 @@ public VT_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : b public override string ReadScalarValue(BinaryReader br) { uint size = br.ReadUInt32(); - data = br.ReadBytes((int)size); + byte[] data = br.ReadBytes((int)size); string result = Encoding.GetEncoding(codePage).GetString(data); //result = result.Trim(new char[] { '\0' }); @@ -363,7 +314,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) } else if (codePage == CodePages.WinUnicode) { - data = Encoding.GetEncoding(codePage).GetBytes(pValue); + byte[] data = Encoding.GetEncoding(codePage).GetBytes(pValue); //if (data.Length >= 2 && data[data.Length - 2] == '\0' && data[data.Length - 1] == '\0') // addNullTerminator = false; @@ -389,7 +340,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) } else { - data = Encoding.GetEncoding(codePage).GetBytes(pValue); + byte[] data = Encoding.GetEncoding(codePage).GetBytes(pValue); //if (data.Length >= 1 && data[data.Length - 1] == '\0') // addNullTerminator = false; @@ -415,7 +366,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) } } - protected class VT_Unaligned_LPSTR_Property : VT_LPSTR_Property + protected sealed class VT_Unaligned_LPSTR_Property : VT_LPSTR_Property { public VT_Unaligned_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, codePage, isVariant) { @@ -423,9 +374,8 @@ public VT_Unaligned_LPSTR_Property(VTPropertyType vType, int codePage, bool isVa } } - private class VT_LPWSTR_Property : TypedPropertyValue + private sealed class VT_LPWSTR_Property : TypedPropertyValue { - private byte[] data; private readonly int codePage; public VT_LPWSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, isVariant) @@ -436,7 +386,7 @@ public VT_LPWSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : public override string ReadScalarValue(BinaryReader br) { uint nChars = br.ReadUInt32(); - data = br.ReadBytes((int)((nChars - 1) * 2)); //WChar- null terminator + byte[] data = br.ReadBytes((int)((nChars - 1) * 2)); //WChar- null terminator br.ReadBytes(2); // Skip null terminator string result = Encoding.Unicode.GetString(data); //result = result.Trim(new char[] { '\0' }); @@ -446,7 +396,7 @@ public override string ReadScalarValue(BinaryReader br) public override void WriteScalarValue(BinaryWriter bw, string pValue) { - data = Encoding.Unicode.GetBytes(pValue); + byte[] data = Encoding.Unicode.GetBytes(pValue); // The written data length field is the number of characters (not bytes) and must include a null terminator // add a null terminator if there isn't one already @@ -473,7 +423,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) } } - private class VT_FILETIME_Property : TypedPropertyValue + private sealed class VT_FILETIME_Property : TypedPropertyValue { public VT_FILETIME_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -486,13 +436,10 @@ public override DateTime ReadScalarValue(BinaryReader br) return DateTime.FromFileTimeUtc(tmp); } - public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) - { - bw.Write(pValue.ToFileTimeUtc()); - } + public override void WriteScalarValue(BinaryWriter bw, DateTime pValue) => bw.Write(pValue.ToFileTimeUtc()); } - private class VT_DECIMAL_Property : TypedPropertyValue + private sealed class VT_DECIMAL_Property : TypedPropertyValue { public VT_DECIMAL_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -535,7 +482,7 @@ public override void WriteScalarValue(BinaryWriter bw, decimal pValue) } } - private class VT_BOOL_Property : TypedPropertyValue + private sealed class VT_BOOL_Property : TypedPropertyValue { public VT_BOOL_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -548,13 +495,10 @@ public override bool ReadScalarValue(BinaryReader br) //br.ReadUInt16();//padding } - public override void WriteScalarValue(BinaryWriter bw, bool pValue) - { - bw.Write(pValue ? (ushort)0xFFFF : (ushort)0); - } + public override void WriteScalarValue(BinaryWriter bw, bool pValue) => bw.Write(pValue ? (ushort)0xFFFF : (ushort)0); } - private class VT_CF_Property : TypedPropertyValue + private sealed class VT_CF_Property : TypedPropertyValue { public VT_CF_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -582,7 +526,7 @@ public override void WriteScalarValue(BinaryWriter bw, object pValue) } } - private class VT_BLOB_Property : TypedPropertyValue + private sealed class VT_BLOB_Property : TypedPropertyValue { public VT_BLOB_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -609,7 +553,7 @@ public override void WriteScalarValue(BinaryWriter bw, object pValue) } } - private class VT_CLSID_Property : TypedPropertyValue + private sealed class VT_CLSID_Property : TypedPropertyValue { public VT_CLSID_Property(VTPropertyType vType, bool isVariant) : base(vType, isVariant) { @@ -628,7 +572,7 @@ public override void WriteScalarValue(BinaryWriter bw, object pValue) } } - private class VT_VariantVector : TypedPropertyValue + private sealed class VT_VariantVector : TypedPropertyValue { private readonly int codePage; private readonly PropertyFactory factory; @@ -647,7 +591,7 @@ public override object ReadScalarValue(BinaryReader br) VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); br.ReadUInt16(); // Ushort Padding - ITypedPropertyValue p = factory.NewProperty(vType, codePage, propertyIdentifier, true); + ITypedPropertyValue p = factory.CreateProperty(vType, codePage, propertyIdentifier, true); p.Read(br); return p; } diff --git a/OpenMcdf.Ole/ProperyIdentifiers.cs b/OpenMcdf.Ole/PropertyIdentifiers.cs similarity index 62% rename from OpenMcdf.Ole/ProperyIdentifiers.cs rename to OpenMcdf.Ole/PropertyIdentifiers.cs index 6dd53bf3..de87ae7a 100644 --- a/OpenMcdf.Ole/ProperyIdentifiers.cs +++ b/OpenMcdf.Ole/PropertyIdentifiers.cs @@ -4,7 +4,7 @@ namespace OpenMcdf.Ole; public static class PropertyIdentifiers { - public static ImmutableDictionary SummaryInfo { get; } = new Dictionary() + readonly static ImmutableDictionary SummaryInfo = new Dictionary() { {0x00000001, "CodePageString" }, {0x00000002, "PIDSI_TITLE" }, @@ -15,7 +15,6 @@ public static class PropertyIdentifiers {0x00000007, "PIDSI_TEMPLATE" }, {0x00000008, "PIDSI_LASTAUTHOR" }, {0x00000009, "PIDSI_REVNUMBER" }, - {0x00000012, "PIDSI_APPNAME" }, {0x0000000A, "PIDSI_EDITTIME" }, {0x0000000B, "PIDSI_LASTPRINTED" }, {0x0000000C, "PIDSI_CREATE_DTM" }, @@ -23,10 +22,11 @@ public static class PropertyIdentifiers {0x0000000E, "PIDSI_PAGECOUNT" }, {0x0000000F, "PIDSI_WORDCOUNT" }, {0x00000010, "PIDSI_CHARCOUNT" }, + {0x00000012, "PIDSI_APPNAME" }, {0x00000013, "PIDSI_DOC_SECURITY" } }.ToImmutableDictionary(); - public static ImmutableDictionary DocumentSummaryInfo { get; } = new Dictionary() + readonly static ImmutableDictionary DocumentSummaryInfo = new Dictionary() { {0x00000001, "CodePageString" }, {0x00000002, "PIDDSI_CATEGORY" }, @@ -45,4 +45,28 @@ public static class PropertyIdentifiers {0x0000000F, "PIDDSI_COMPANY" }, {0x00000010, "PIDDSI_LINKSDIRTY" } }.ToImmutableDictionary(); + + public static string GetDescription(uint identifier, ContainerType map, IDictionary? customDictionary = null) + { + IDictionary nameDictionary; + + if (customDictionary is null) + { + nameDictionary = map switch + { + ContainerType.SummaryInfo => SummaryInfo, + ContainerType.DocumentSummaryInfo => DocumentSummaryInfo, + _ => throw new ArgumentException("Unknown container type", nameof(map)), + }; + } + else + { + nameDictionary = customDictionary; + } + + if (nameDictionary.TryGetValue(identifier, out string? value)) + return value; + + return $"0x{identifier:x8}"; + } } diff --git a/OpenMcdf.Ole/PropertySet.cs b/OpenMcdf.Ole/PropertySet.cs index dcc8f990..196cf0d9 100644 --- a/OpenMcdf.Ole/PropertySet.cs +++ b/OpenMcdf.Ole/PropertySet.cs @@ -2,31 +2,34 @@ internal sealed class PropertySet { - public PropertyContext PropertyContext - { - get; set; - } + public PropertyContext PropertyContext { get; set; } = new(); public uint Size { get; set; } - public uint NumProperties { get; set; } + public List PropertyIdentifierAndOffsets { get; } = new(); - public List PropertyIdentifierAndOffsets { get; set; } = new List(); - - public List Properties { get; set; } = new List(); + public List Properties { get; } = new(); public void LoadContext(int propertySetOffset, BinaryReader br) { long currPos = br.BaseStream.Position; - - PropertyContext = new PropertyContext(); - int codePageOffset = (int)(propertySetOffset + PropertyIdentifierAndOffsets.Where(pio => pio.PropertyIdentifier == 1).First().Offset); + int codePageOffset = (int)(propertySetOffset + PropertyIdentifierAndOffsets.First(pio => pio.PropertyIdentifier == 1).Offset); br.BaseStream.Seek(codePageOffset, SeekOrigin.Begin); - VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); + var vType = (VTPropertyType)br.ReadUInt16(); br.ReadUInt16(); // Ushort Padding PropertyContext.CodePage = (ushort)br.ReadInt16(); br.BaseStream.Position = currPos; } + + public void Add(IDictionary propertyNames) + { + DictionaryProperty dictionaryProperty = new(PropertyContext.CodePage) + { + Value = propertyNames + }; + Properties.Add(dictionaryProperty); + PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = 0, Offset = 0 }); + } } diff --git a/OpenMcdf.Ole/PropertySetStream.cs b/OpenMcdf.Ole/PropertySetStream.cs index 3c3acb97..2f3de824 100644 --- a/OpenMcdf.Ole/PropertySetStream.cs +++ b/OpenMcdf.Ole/PropertySetStream.cs @@ -2,6 +2,14 @@ internal sealed class PropertySetStream { + private sealed class OffsetContainer + { + public int OffsetPS { get; set; } + + public List PropertyIdentifierOffsets { get; } = new(); + public List PropertyOffsets { get; } = new(); + } + public ushort ByteOrder { get; set; } public ushort Version { get; set; } public uint SystemIdentifier { get; set; } @@ -11,10 +19,8 @@ internal sealed class PropertySetStream public uint Offset0 { get; set; } public Guid FMTID1 { get; set; } public uint Offset1 { get; set; } - public PropertySet PropertySet0 { get; set; } - public PropertySet PropertySet1 { get; set; } - - //private SummaryInfoMap map; + public PropertySet? PropertySet0 { get; set; } + public PropertySet? PropertySet1 { get; set; } public PropertySetStream() { @@ -36,17 +42,19 @@ public void Read(BinaryReader br) Offset1 = br.ReadUInt32(); } + + uint size = br.ReadUInt32(); + uint propertyCount = br.ReadUInt32(); PropertySet0 = new PropertySet { - Size = br.ReadUInt32(), - NumProperties = br.ReadUInt32() + Size = size, }; // Create appropriate property factory based on the stream type - PropertyFactory factory = FMTID0 == WellKnownFormatIdentifiers.DocSummaryInformation ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; + PropertyFactory factory = FMTID0 == FormatIdentifiers.DocSummaryInformation ? DocumentSummaryInfoPropertyFactory.Default : DefaultPropertyFactory.Default; // Read property offsets (P0) - for (int i = 0; i < PropertySet0.NumProperties; i++) + for (int i = 0; i < propertyCount; i++) { PropertyIdentifierAndOffset pio = new() { @@ -59,23 +67,28 @@ public void Read(BinaryReader br) PropertySet0.LoadContext((int)Offset0, br); //Read CodePage, Locale // Read properties (P0) - for (int i = 0; i < PropertySet0.NumProperties; i++) + for (int i = 0; i < propertyCount; i++) { - br.BaseStream.Seek(Offset0 + PropertySet0.PropertyIdentifierAndOffsets[i].Offset, SeekOrigin.Begin); - PropertySet0.Properties.Add(ReadProperty(PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet0.PropertyContext.CodePage, br, factory)); + PropertyIdentifierAndOffset propertyIdentifierAndOffset = PropertySet0.PropertyIdentifierAndOffsets[i]; + br.BaseStream.Seek(Offset0 + propertyIdentifierAndOffset.Offset, SeekOrigin.Begin); + IProperty property = ReadProperty(propertyIdentifierAndOffset.PropertyIdentifier, PropertySet0.PropertyContext.CodePage, br, factory); + PropertySet0.Properties.Add(property); } if (NumPropertySets == 2) { br.BaseStream.Seek(Offset1, SeekOrigin.Begin); + + size = br.ReadUInt32(); + propertyCount = br.ReadUInt32(); + PropertySet1 = new PropertySet { - Size = br.ReadUInt32(), - NumProperties = br.ReadUInt32() + Size = size }; // Read property offsets - for (int i = 0; i < PropertySet1.NumProperties; i++) + for (int i = 0; i < propertyCount; i++) { PropertyIdentifierAndOffset pio = new() { @@ -88,32 +101,20 @@ public void Read(BinaryReader br) PropertySet1.LoadContext((int)Offset1, br); // Read properties - for (int i = 0; i < PropertySet1.NumProperties; i++) + for (int i = 0; i < propertyCount; i++) { - br.BaseStream.Seek(Offset1 + PropertySet1.PropertyIdentifierAndOffsets[i].Offset, SeekOrigin.Begin); - PropertySet1.Properties.Add(ReadProperty(PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet1.PropertyContext.CodePage, br, DefaultPropertyFactory.Instance)); + PropertyIdentifierAndOffset idAndOffset = PropertySet1.PropertyIdentifierAndOffsets[i]; + br.BaseStream.Seek(Offset1 + idAndOffset.Offset, SeekOrigin.Begin); + IProperty property = ReadProperty(idAndOffset.PropertyIdentifier, PropertySet1.PropertyContext.CodePage, br, DefaultPropertyFactory.Default); + PropertySet1.Properties.Add(property); } } } - private class OffsetContainer - { - public int OffsetPS { get; set; } - - public List PropertyIdentifierOffsets { get; set; } - public List PropertyOffsets { get; set; } - - public OffsetContainer() - { - PropertyOffsets = new List(); - PropertyIdentifierOffsets = new List(); - } - } - public void Write(BinaryWriter bw) { - var oc0 = new OffsetContainer(); - var oc1 = new OffsetContainer(); + OffsetContainer oc0 = new(); + OffsetContainer oc1 = new(); bw.Write(ByteOrder); bw.Write(Version); @@ -130,17 +131,17 @@ public void Write(BinaryWriter bw) } oc0.OffsetPS = (int)bw.BaseStream.Position; - bw.Write(PropertySet0.Size); - bw.Write(PropertySet0.NumProperties); + bw.Write(PropertySet0!.Size); + bw.Write(PropertySet0.Properties.Count); // w property offsets - for (int i = 0; i < PropertySet0.NumProperties; i++) + for (int i = 0; i < PropertySet0.Properties.Count; i++) { - oc0.PropertyIdentifierOffsets.Add(bw.BaseStream.Position); //Offset of 4 to Offset value + oc0.PropertyIdentifierOffsets.Add(bw.BaseStream.Position); // Offset of 4 to Offset value PropertySet0.PropertyIdentifierAndOffsets[i].Write(bw); } - for (int i = 0; i < PropertySet0.NumProperties; i++) + for (int i = 0; i < PropertySet0.Properties.Count; i++) { oc0.PropertyOffsets.Add(bw.BaseStream.Position); PropertySet0.Properties[i].Write(bw); @@ -160,8 +161,8 @@ public void Write(BinaryWriter bw) { oc1.OffsetPS = (int)bw.BaseStream.Position; - bw.Write(PropertySet1.Size); - bw.Write(PropertySet1.NumProperties); + bw.Write(PropertySet1!.Size); + bw.Write(PropertySet1.Properties.Count); // w property offsets for (int i = 0; i < PropertySet1.PropertyIdentifierAndOffsets.Count; i++) @@ -170,7 +171,7 @@ public void Write(BinaryWriter bw) PropertySet1.PropertyIdentifierAndOffsets[i].Write(bw); } - for (int i = 0; i < PropertySet1.NumProperties; i++) + for (int i = 0; i < PropertySet1.Properties.Count; i++) { oc1.PropertyOffsets.Add(bw.BaseStream.Position); PropertySet1.Properties[i].Write(bw); @@ -203,7 +204,7 @@ public void Write(BinaryWriter bw) bw.Write((int)(oc0.PropertyOffsets[i] - oc0.OffsetPS)); } - if (PropertySet1 != null) + if (PropertySet1 is not null) { for (int i = 0; i < PropertySet1.PropertyIdentifierAndOffsets.Count; i++) { @@ -215,21 +216,19 @@ public void Write(BinaryWriter bw) private static IProperty ReadProperty(uint propertyIdentifier, int codePage, BinaryReader br, PropertyFactory factory) { - if (propertyIdentifier != 0) - { - var vType = (VTPropertyType)br.ReadUInt16(); - br.ReadUInt16(); // Ushort Padding - - ITypedPropertyValue pr = factory.NewProperty(vType, codePage, propertyIdentifier); - pr.Read(br); - - return pr; - } - else + if (propertyIdentifier == 0) { DictionaryProperty dictionaryProperty = new(codePage); dictionaryProperty.Read(br); return dictionaryProperty; } + + var vType = (VTPropertyType)br.ReadUInt16(); + br.ReadUInt16(); // Ushort Padding + + ITypedPropertyValue pr = factory.CreateProperty(vType, codePage, propertyIdentifier); + pr.Read(br); + + return pr; } } diff --git a/OpenMcdf.Ole/TypedPropertyValue.cs b/OpenMcdf.Ole/TypedPropertyValue.cs index 80d0e447..6bd2e11a 100644 --- a/OpenMcdf.Ole/TypedPropertyValue.cs +++ b/OpenMcdf.Ole/TypedPropertyValue.cs @@ -3,13 +3,12 @@ internal abstract class TypedPropertyValue : ITypedPropertyValue { private readonly VTPropertyType _VTType; + protected object? propertyValue; public PropertyType PropertyType => PropertyType.TypedPropertyValue; public VTPropertyType VTType => _VTType; - protected object propertyValue; - public TypedPropertyValue(VTPropertyType vtType, bool isVariant = false) { _VTType = vtType; @@ -23,7 +22,7 @@ public TypedPropertyValue(VTPropertyType vtType, bool isVariant = false) protected virtual bool NeedsPadding { get; set; } = true; - private PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType) + private static PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType) { if ((((ushort)vtType) & 0x1000) != 0) return PropertyDimensions.IsVector; @@ -32,14 +31,13 @@ private PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType) return PropertyDimensions.IsScalar; } - public virtual object Value + public virtual object? Value { get => propertyValue; - set => propertyValue = value; } - public abstract T ReadScalarValue(BinaryReader br); + public abstract T? ReadScalarValue(BinaryReader br); public void Read(BinaryReader br) { @@ -64,11 +62,11 @@ public void Read(BinaryReader br) { uint nItems = br.ReadUInt32(); - List res = new List(); + List res = new(); for (int i = 0; i < nItems; i++) { - T s = ReadScalarValue(br); + T s = ReadScalarValue(br)!; res.Add(s); @@ -108,7 +106,7 @@ public void Write(BinaryWriter bw) bw.Write((ushort)_VTType); bw.Write((ushort)0); - WriteScalarValue(bw, (T)propertyValue); + WriteScalarValue(bw, (T)propertyValue!); size = (int)(bw.BaseStream.Position - currentPos); m = size % 4; @@ -124,7 +122,7 @@ public void Write(BinaryWriter bw) bw.Write((ushort)_VTType); bw.Write((ushort)0); - bw.Write((uint)((List)propertyValue).Count); + bw.Write((uint)((List)propertyValue!).Count); for (int i = 0; i < ((List)propertyValue).Count; i++) { diff --git a/OpenMcdf.Ole/VTPropertyType.cs b/OpenMcdf.Ole/VTPropertyType.cs index f6084dd3..67562fd5 100644 --- a/OpenMcdf.Ole/VTPropertyType.cs +++ b/OpenMcdf.Ole/VTPropertyType.cs @@ -1,5 +1,7 @@ namespace OpenMcdf.Ole; +#pragma warning disable CA1707 // Remove the underscores from member name + /// /// VARENUM /// From 7d68c67753cd60be41ccfe2d4669e1bdeb3390be Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 11:08:27 +1300 Subject: [PATCH 098/114] Allow opening old word docs --- OpenMcdf3/CfbBinaryReader.cs | 5 +++-- OpenMcdf3/DirectoryEntry.cs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf3/CfbBinaryReader.cs index 5ffdb220..2d1e00a2 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf3/CfbBinaryReader.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Diagnostics; +using System.Text; #if NETSTANDARD2_0 using System.Buffers; @@ -78,7 +79,7 @@ public Header ReadHeader() if (header.MajorVersion is not (ushort)Version.V3 and not (ushort)Version.V4) throw new FormatException($"Unsupported major version: {header.MajorVersion}."); else if (header.MinorVersion is not Header.ExpectedMinorVersion) - throw new FormatException($"Unsupported minor version: {header.MinorVersion}."); + Trace.WriteLine($"Unexpected minor version: {header.MinorVersion}."); ushort byteOrder = ReadUInt16(); if (byteOrder != Header.LittleEndian) throw new FormatException($"Unsupported byte order: {byteOrder:X4}. Only little-endian is supported ({Header.LittleEndian:X4})."); diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index dadf45df..fff18c13 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -211,6 +211,7 @@ public void Recycle(StorageType storageType, string name) { StorageType.Stream => EntryType.Stream, StorageType.Storage => EntryType.Storage, + StorageType.Root => EntryType.Storage, _ => throw new InvalidOperationException("Invalid storage type.") }; From 2a6b0a30502775148e961d83b4e621b45fb18eb6 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 14:18:03 +1300 Subject: [PATCH 099/114] Fix warnings --- OpenMcdf3/DirectoryEntry.cs | 11 ++++++++ OpenMcdf3/Header.cs | 28 ++++++++++++++----- .../StreamDataProvider.cs | 4 +-- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf3/DirectoryEntry.cs index fff18c13..8b9b36f1 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf3/DirectoryEntry.cs @@ -154,11 +154,22 @@ public string NameString set => NameLength = (ushort)(Encoding.Unicode.GetBytes(value, 0, value.Length, Name, 0) + 2); } + public override int GetHashCode() + { + HashCode code = new(); + code.Add(Id); + code.Add(NameLength); + foreach (byte b in Name) + code.Add(b); + return code.GetHashCode(); + } + public override bool Equals(object? obj) => Equals(obj as DirectoryEntry); public bool Equals(DirectoryEntry? other) { return other is not null + && Id == other.Id && Name.SequenceEqual(other.Name) && NameLength == other.NameLength && Type == other.Type diff --git a/OpenMcdf3/Header.cs b/OpenMcdf3/Header.cs index 3c99d722..900a2936 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf3/Header.cs @@ -139,6 +139,27 @@ public Header(Version version = Version.V3) } } + public override int GetHashCode() + { + HashCode code = new(); + code.Add(CLSID); + code.Add(MinorVersion); + code.Add(MajorVersion); + code.Add(SectorShift); + code.Add(DirectorySectorCount); + code.Add(FatSectorCount); + code.Add(FirstDirectorySectorId); + code.Add(TransactionSignature); + code.Add(FatSectorCount); + code.Add(FirstMiniFatSectorId); + code.Add(MiniFatSectorCount); + code.Add(FirstDifatSectorId); + code.Add(DifatSectorCount); + foreach (uint value in Difat) + code.Add(value); + return code.ToHashCode(); + } + public override bool Equals(object? obj) => Equals(obj as Header); public bool Equals(Header? other) @@ -159,12 +180,5 @@ public bool Equals(Header? other) && Difat.SequenceEqual(other.Difat); } - public override int GetHashCode() - { - return HashCode.Combine( - HashCode.Combine(CLSID, MinorVersion, MajorVersion, SectorShift, DirectorySectorCount, FatSectorCount, FirstDirectorySectorId, TransactionSignature), - HashCode.Combine(FirstMiniFatSectorId, MiniFatSectorCount, FirstDifatSectorId, DifatSectorCount, Difat)); - } - public override string ToString() => $"MajorVersion: {MajorVersion}, MinorVersion: {MinorVersion}, FirstDirectorySectorId: {FirstDirectorySectorId}, FirstMiniFatSectorId: {FirstMiniFatSectorId}"; } diff --git a/StructuredStorageExplorer/StreamDataProvider.cs b/StructuredStorageExplorer/StreamDataProvider.cs index ffecb71d..80aab085 100644 --- a/StructuredStorageExplorer/StreamDataProvider.cs +++ b/StructuredStorageExplorer/StreamDataProvider.cs @@ -74,12 +74,12 @@ public void ApplyChanges() /// /// Occurs, when the write buffer contains new changes. /// - public event EventHandler Changed; + public event EventHandler? Changed; /// /// Occurs, when InsertBytes or DeleteBytes method is called. /// - public event EventHandler LengthChanged; + public event EventHandler? LengthChanged; /// /// Reads a byte from the byte collection. From 97ca43a985d6f10f785c983418aaa3ce9d0eb5d6 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 14:28:02 +1300 Subject: [PATCH 100/114] Refactor explorer --- StructuredStorageExplorer/MainForm.cs | 185 ++++++++++++-------------- StructuredStorageExplorer/Utils.cs | 24 ++-- 2 files changed, 95 insertions(+), 114 deletions(-) diff --git a/StructuredStorageExplorer/MainForm.cs b/StructuredStorageExplorer/MainForm.cs index 812af78d..4e64cb00 100644 --- a/StructuredStorageExplorer/MainForm.cs +++ b/StructuredStorageExplorer/MainForm.cs @@ -12,7 +12,33 @@ namespace StructuredStorageExplorer; -record class NodeSelection(Storage Parent, EntryInfo EntryInfo); +sealed record class NodeSelection(Storage? Parent, EntryInfo EntryInfo) +{ + public string SanitizedFileName + { + get + { + // A lot of stream and storage have only non-printable characters. + // We need to sanitize filename. + + string sanitizedFileName = string.Empty; + + foreach (char c in EntryInfo.Name) + { + UnicodeCategory category = char.GetUnicodeCategory(c); + if (category is UnicodeCategory.LetterNumber or UnicodeCategory.LowercaseLetter or UnicodeCategory.UppercaseLetter) + sanitizedFileName += c; + } + + if (string.IsNullOrEmpty(sanitizedFileName)) + { + sanitizedFileName = "tempFileName"; + } + + return $"{sanitizedFileName}.bin"; + } + } +} /// /// Sample Structured Storage viewer to @@ -21,8 +47,6 @@ record class NodeSelection(Storage Parent, EntryInfo EntryInfo); public partial class MainForm : Form { private RootStorage? cf; - private FileStream? fs; - private bool canUpdate; public MainForm() { @@ -32,15 +56,10 @@ public MainForm() tabControl1.TabPages.Remove(tabPage2); #endif - //Load images for icons from resx - Image folderImage = (Image)Resources.ResourceManager.GetObject("storage"); - Image streamImage = (Image)Resources.ResourceManager.GetObject("stream"); - //Image olePropsImage = (Image)Properties.Resources.ResourceManager.GetObject("oleprops"); - - treeView1.ImageList = new ImageList(); - treeView1.ImageList.Images.Add(folderImage); - treeView1.ImageList.Images.Add(streamImage); - //treeView1.ImageList.Images.Add(olePropsImage); + ImageList imageList = new(); + imageList.Images.Add(Resources.storage); + imageList.Images.Add(Resources.stream); + treeView1.ImageList = imageList; saveAsToolStripMenuItem.Enabled = false; updateCurrentFileToolStripMenuItem.Enabled = false; @@ -52,12 +71,7 @@ private void OpenFile() { CloseCurrentFile(); - treeView1.Nodes.Clear(); - fileNameLabel.Text = openFileDialog1.FileName; - LoadFile(openFileDialog1.FileName, true); - canUpdate = true; - saveAsToolStripMenuItem.Enabled = true; - updateCurrentFileToolStripMenuItem.Enabled = true; + LoadFile(openFileDialog1.FileName); } } @@ -66,9 +80,6 @@ private void CloseCurrentFile() cf?.Dispose(); cf = null; - fs?.Close(); - fs = null; - treeView1.Nodes.Clear(); fileNameLabel.Text = string.Empty; saveAsToolStripMenuItem.Enabled = false; @@ -87,56 +98,62 @@ private void CreateNewFile() { CloseCurrentFile(); - cf = RootStorage.Create(Path.GetTempFileName()); - canUpdate = false; - saveAsToolStripMenuItem.Enabled = true; + try + { + string fileName = Path.GetTempFileName(); - updateCurrentFileToolStripMenuItem.Enabled = false; + cf = RootStorage.Create(fileName); - RefreshTree(); + fileNameLabel.Text = fileName; + saveAsToolStripMenuItem.Enabled = true; + updateCurrentFileToolStripMenuItem.Enabled = true; + + RefreshTree(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or FormatException) + { + CloseCurrentFile(); + + MessageBox.Show($"Error creating file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } } private void RefreshTree() { treeView1.Nodes.Clear(); - TreeNode root = treeView1.Nodes.Add("Root Entry", "Root"); - root.ImageIndex = 0; - root.Tag = new NodeSelection(null, cf.EntryInfo); - // Recursive function to get all storage and streams - AddNodes(root, cf); + if (cf is not null) + { + TreeNode root = treeView1.Nodes.Add(cf.EntryInfo.Name); + root.ImageIndex = 0; + root.Tag = new NodeSelection(null, cf.EntryInfo); + + // Recursive function to get all storage and streams + AddNodes(root, cf); + } } - private void LoadFile(string fileName, bool enableCommit) + private void LoadFile(string fileName) { - fs = new FileStream( - fileName, - FileMode.Open, - enableCommit ? - FileAccess.ReadWrite - : FileAccess.Read); - try { cf?.Dispose(); cf = null; // Load file - cf = RootStorage.Open(fs, enableCommit ? StorageModeFlags.Transacted : StorageModeFlags.None); + cf = RootStorage.Open(fileName, FileMode.Open); + + fileNameLabel.Text = fileName; + saveAsToolStripMenuItem.Enabled = true; + updateCurrentFileToolStripMenuItem.Enabled = true; RefreshTree(); } - catch (Exception ex) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or FormatException) { - cf?.Dispose(); - cf = null; - - fs?.Close(); - fs = null; + CloseCurrentFile(); - treeView1.Nodes.Clear(); - fileNameLabel.Text = string.Empty; - MessageBox.Show("Internal error: " + ex.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"Error opening file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } @@ -150,22 +167,18 @@ private static void AddNodes(TreeNode node, Storage storage) foreach (EntryInfo item in storage.EnumerateEntries()) { TreeNode childNode = node.Nodes.Add(item.Name); - childNode.Tag = new NodeSelection(storage, item); if (item.Type is EntryType.Storage) { - // Storage childNode.ImageIndex = 0; childNode.SelectedImageIndex = 0; Storage subStorage = storage.OpenStorage(item.Name); - // Recursion into the storage AddNodes(childNode, subStorage); } else { - // Stream childNode.ImageIndex = 1; childNode.SelectedImageIndex = 1; } @@ -175,31 +188,13 @@ private static void AddNodes(TreeNode node, Storage storage) private void exportDataToolStripMenuItem_Click(object sender, EventArgs e) { // No export if storage - NodeSelection? selection = treeView1.SelectedNode?.Tag as NodeSelection; - if (selection is null || selection.EntryInfo.Type is not EntryType.Stream) + if (treeView1.SelectedNode?.Tag is not NodeSelection selection || selection.EntryInfo.Type is not EntryType.Stream || selection.Parent is null) { MessageBox.Show("Only stream data can be exported", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } - // A lot of stream and storage have only non-printable characters. - // We need to sanitize filename. - - string sanitizedFileName = string.Empty; - - foreach (char c in selection.EntryInfo.Name) - { - UnicodeCategory category = char.GetUnicodeCategory(c); - if (category is UnicodeCategory.LetterNumber or UnicodeCategory.LowercaseLetter or UnicodeCategory.UppercaseLetter) - sanitizedFileName += c; - } - - if (string.IsNullOrEmpty(sanitizedFileName)) - { - sanitizedFileName = "tempFileName"; - } - - saveFileDialog1.FileName = $"{sanitizedFileName}.bin"; + saveFileDialog1.FileName = selection.SanitizedFileName; if (saveFileDialog1.ShowDialog() == DialogResult.OK) { @@ -209,18 +204,16 @@ private void exportDataToolStripMenuItem_Click(object sender, EventArgs e) using CfbStream cfbStream = selection.Parent.OpenStream(selection.EntryInfo.Name); cfbStream.CopyTo(fs); } - catch (Exception ex) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - treeView1.Nodes.Clear(); - MessageBox.Show($"Internal error: {ex.Message}", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"Error saving file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void removeToolStripMenuItem_Click(object sender, EventArgs e) { - TreeNode n = treeView1.SelectedNode; - if (n?.Parent?.Tag is NodeSelection selection && selection.Parent is not null) + if (treeView1.SelectedNode?.Tag is NodeSelection selection && selection.Parent is not null) selection.Parent.Delete(selection.EntryInfo.Name); RefreshTree(); @@ -237,16 +230,12 @@ private void saveAsToolStripMenuItem_Click(object sender, EventArgs e) private void updateCurrentFileToolStripMenuItem_Click(object sender, EventArgs e) { - if (canUpdate) - { - if (hexEditor.ByteProvider is not null && hexEditor.ByteProvider.HasChanges()) - hexEditor.ByteProvider.ApplyChanges(); - cf.Commit(); - } - else - { - MessageBox.Show("Cannot update a compound document that is not based on a stream or on a file", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } + if (cf is null) + return; + + if (hexEditor.ByteProvider is not null && hexEditor.ByteProvider.HasChanges()) + hexEditor.ByteProvider.ApplyChanges(); + cf.Commit(); } private void addStreamToolStripMenuItem_Click(object sender, EventArgs e) @@ -303,8 +292,7 @@ private void importDataStripMenuItem1_Click(object sender, EventArgs e) private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { - cf?.Dispose(); - cf = null; + CloseCurrentFile(); } private void contextMenuStrip1_Opening(object sender, CancelEventArgs e) @@ -365,7 +353,7 @@ private void treeView1_MouseUp(object sender, MouseEventArgs e) if (nodeSelection.EntryInfo.Type is EntryType.Stream) { - using CfbStream stream = nodeSelection.Parent.OpenStream(nodeSelection.EntryInfo.Name); + using CfbStream stream = nodeSelection.Parent!.OpenStream(nodeSelection.EntryInfo.Name); addStorageStripMenuItem1.Enabled = false; addStreamToolStripMenuItem.Enabled = false; importDataStripMenuItem1.Enabled = true; @@ -384,18 +372,11 @@ private void treeView1_MouseUp(object sender, MouseEventArgs e) propertyGrid1.SelectedObject = nodeSelection.EntryInfo; } - catch (Exception ex) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or FormatException) { - cf?.Dispose(); - cf = null; - - fs?.Close(); - fs = null; - - treeView1.Nodes.Clear(); - fileNameLabel.Text = string.Empty; + CloseCurrentFile(); - MessageBox.Show($"Internal error: {ex.Message}", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } @@ -435,7 +416,7 @@ private void UpdateOleTab(CfbStream stream) ds.AcceptChanges(); dgvOLEProps.DataSource = ds; - if (c.HasUserDefinedProperties) + if (c.UserDefinedProperties is not null) { DataTable ds2 = new(); ds2.Columns.Add("Name", typeof(string)); diff --git a/StructuredStorageExplorer/Utils.cs b/StructuredStorageExplorer/Utils.cs index 5ecc3b14..def2be8c 100644 --- a/StructuredStorageExplorer/Utils.cs +++ b/StructuredStorageExplorer/Utils.cs @@ -1,42 +1,42 @@ namespace StructuredStorageExplorer; -class Utils +static class Utils { public static DialogResult InputBox(string title, string promptText, ref string value) { - Form form = new Form(); - Label label = new Label(); - TextBox textBox = new TextBox(); - Button buttonOk = new Button(); - Button buttonCancel = new Button(); + using Form form = new(); + using Label label = new(); + using TextBox textBox = new(); + using Button buttonOK = new(); + using Button buttonCancel = new(); form.Text = title; label.Text = promptText; textBox.Text = value; - buttonOk.Text = "OK"; + buttonOK.Text = "OK"; buttonCancel.Text = "Cancel"; - buttonOk.DialogResult = DialogResult.OK; + buttonOK.DialogResult = DialogResult.OK; buttonCancel.DialogResult = DialogResult.Cancel; label.SetBounds(9, 20, 372, 13); textBox.SetBounds(12, 36, 372, 20); - buttonOk.SetBounds(228, 72, 75, 23); + buttonOK.SetBounds(228, 72, 75, 23); buttonCancel.SetBounds(309, 72, 75, 23); label.AutoSize = true; textBox.Anchor = textBox.Anchor | AnchorStyles.Right; - buttonOk.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + buttonOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; buttonCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; form.ClientSize = new Size(396, 107); - form.Controls.AddRange([label, textBox, buttonOk, buttonCancel]); + form.Controls.AddRange([label, textBox, buttonOK, buttonCancel]); form.ClientSize = new Size(Math.Max(300, label.Right + 10), form.ClientSize.Height); form.FormBorderStyle = FormBorderStyle.FixedDialog; form.StartPosition = FormStartPosition.CenterScreen; form.MinimizeBox = false; form.MaximizeBox = false; - form.AcceptButton = buttonOk; + form.AcceptButton = buttonOK; form.CancelButton = buttonCancel; DialogResult dialogResult = form.ShowDialog(); From a6a7ddfb9f7138abb214c692e7226b82f94ee5d3 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 14:53:40 +1300 Subject: [PATCH 101/114] Disable warnings for netstandard2.0 --- .editorconfig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index faaf28ca..9f6b96e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -349,6 +349,10 @@ dotnet_style_qualification_for_property = false:none dotnet_style_qualification_for_method = false:none dotnet_style_qualification_for_event = false:none dotnet_style_prefer_collection_expression = when_types_exactly_match:suggestion +dotnet_diagnostic.CA1513.severity = none +dotnet_diagnostic.CA1510.severity = none +dotnet_diagnostic.CA1847.severity = none +dotnet_diagnostic.CA1512.severity = none [*.{hlsl}] From 0b52b2d17742c7551886e84c0df82da40d199457 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 15:20:26 +1300 Subject: [PATCH 102/114] Enable spell checking --- .editorconfig | 23 +++-------------------- OpenMcdf.Ole/PropertyFactory.cs | 4 ++-- OpenMcdf3.Tests/AssemblyInfo.cs | 2 +- OpenMcdf3.sln | 1 + OpenMcdf3/AssemblyInfo.cs | 2 +- exclusion.dic | 22 ++++++++++++++++++++++ 6 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 exclusion.dic diff --git a/.editorconfig b/.editorconfig index 9f6b96e1..6e5fa16a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,31 +4,14 @@ root = true #### Visual Studio Spell Checker #### [*] -spelling_exclusion_path = .\ignored-words.txt +spelling_exclusion_path = .\exclusion.dic #### VS Spell Checker #### [*] -vsspell_section_id = 57c612f5006c4c25a138fae34ef5a504 +vsspell_section_id = root vsspell_determine_resource_file_language_from_name = true -vsspell_additional_dictionary_folders_57c612f5006c4c25a138fae34ef5a504 = .\dictionaries -vsspell_ignored_words_57c612f5006c4c25a138fae34ef5a504 = clear_inherited|File:ignored-words.txt|Readit - -[*.{cs,da,de,el,es,fr,fi,it,ko,nb-NO,nl-BE,pl,pt,pt-BR,ru,sk,sv}.resx] -vsspell_spell_check_as_you_type = false -vsspell_include_in_project_spell_check = false - -[*.{Designer.cs,ai,crproj,csproj,props,runsettings,targets,vcxproj,wixproj,wxi,wxl,wxs,xaml,xml}] -vsspell_spell_check_as_you_type = false -vsspell_include_in_project_spell_check = false - -[Service References/*/*] -vsspell_spell_check_as_you_type = false -vsspell_include_in_project_spell_check = false - -[{ignored-words.txt,ControlWords.cs,LanguageIdTests.cs,Phoneme.cs,UnicodeData.txt}] -vsspell_spell_check_as_you_type = false -vsspell_include_in_project_spell_check = false +vsspell_ignored_words_root = clear_inherited|File:exclusion.dic # C# files [*.cs] diff --git a/OpenMcdf.Ole/PropertyFactory.cs b/OpenMcdf.Ole/PropertyFactory.cs index 968e7f30..f8a699f1 100644 --- a/OpenMcdf.Ole/PropertyFactory.cs +++ b/OpenMcdf.Ole/PropertyFactory.cs @@ -326,7 +326,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) // var mod = dataLength % 4; // pad to multiple of 4 bytes - bw.Write(dataLength); // datalength of string + null char (unicode) + bw.Write(dataLength); // data length of string + null char (unicode) bw.Write(data); // string //if (addNullTerminator) @@ -352,7 +352,7 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) uint mod = dataLength % 4; // pad to multiple of 4 bytes - bw.Write(dataLength); // datalength of string + null char (unicode) + bw.Write(dataLength); // data length of string + null char (unicode) bw.Write(data); // string //if (addNullTerminator) diff --git a/OpenMcdf3.Tests/AssemblyInfo.cs b/OpenMcdf3.Tests/AssemblyInfo.cs index 6df06f30..02626394 100644 --- a/OpenMcdf3.Tests/AssemblyInfo.cs +++ b/OpenMcdf3.Tests/AssemblyInfo.cs @@ -3,7 +3,7 @@ // In SDK-style projects such as this one, several assembly attributes that were historically // defined in this file are now automatically added during build and populated with // values defined in project properties. For details of which attributes are included -// and how to customise this process see: https://aka.ms/assembly-info-properties +// and how to customize this process see: https://aka.ms/assembly-info-properties // Setting ComVisible to false makes the types in this assembly not visible to COM diff --git a/OpenMcdf3.sln b/OpenMcdf3.sln index 9c357779..38cdc0f8 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf3.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .globalconfig = .globalconfig + exclusion.dic = exclusion.dic EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Benchmarks", "OpenMcdf3.Benchmarks\OpenMcdf3.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" diff --git a/OpenMcdf3/AssemblyInfo.cs b/OpenMcdf3/AssemblyInfo.cs index c16894d6..b7349b84 100644 --- a/OpenMcdf3/AssemblyInfo.cs +++ b/OpenMcdf3/AssemblyInfo.cs @@ -4,7 +4,7 @@ // In SDK-style projects such as this one, several assembly attributes that were historically // defined in this file are now automatically added during build and populated with // values defined in project properties. For details of which attributes are included -// and how to customise this process see: https://aka.ms/assembly-info-properties +// and how to customize this process see: https://aka.ms/assembly-info-properties // Setting ComVisible to false makes the types in this assembly not visible to COM diff --git a/exclusion.dic b/exclusion.dic new file mode 100644 index 00000000..7cd19242 --- /dev/null +++ b/exclusion.dic @@ -0,0 +1,22 @@ +Blaseotto +CFB +CLSID +codepage +depersist +DIFAT +endian +enqueuing +enum +enums +Java +LINQ +MCDF +metadata +namespace +namespaces +netstandard +OpenMcdf +toolbar +typelib +Unicode +versioned From 43c449c745979087f66344bf5340606d5b381082 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 15:21:27 +1300 Subject: [PATCH 103/114] Rename to OpenMcdf --- .../FileStreamRead.cs | 4 ++-- .../FileStreamTransactedWrite.cs | 4 ++-- .../FileStreamWrite.cs | 4 ++-- .../MemoryStreamRead.cs | 4 ++-- .../MemoryStreamTransactedWrite.cs | 4 ++-- .../MemoryStreamWrite.cs | 4 ++-- .../OpenMcdf.Benchmarks.csproj | 2 +- .../OpenMcdfBenchmarks.cs | 2 +- .../Program.cs | 2 +- .../StructuredStorageBenchmarks.cs | 2 +- OpenMcdf.Ole/OlePropertiesContainer.cs | 2 +- OpenMcdf.Ole/OpenMcdf.Ole.csproj | 3 ++- .../OpenMcdf.Perf.csproj | 2 +- {OpenMcdf3.Perf => OpenMcdf.Perf}/Program.cs | 2 +- {OpenMcdf3.Tests => OpenMcdf.Tests}/AssemblyInfo.cs | 0 .../BinaryReaderTests.cs | 2 +- .../BinaryWriterTests.cs | 2 +- {OpenMcdf3.Tests => OpenMcdf.Tests}/DebugWriter.cs | 2 +- .../EntryInfoTests.cs | 2 +- .../MultipleStorage.cfs | Bin .../MultipleStorage2.cfs | Bin .../MultipleStorage3.cfs | Bin .../MultipleStorage4.cfs | Bin .../OpenMcdf.Tests.csproj | 2 +- {OpenMcdf3.Tests => OpenMcdf.Tests}/StorageTests.cs | 4 +++- {OpenMcdf3.Tests => OpenMcdf.Tests}/StreamAssert.cs | 2 +- {OpenMcdf3.Tests => OpenMcdf.Tests}/StreamTests.cs | 2 +- .../TestStream_v3_0.cfs | Bin .../TestStream_v3_4095.cfs | Bin .../TestStream_v3_4096.cfs | Bin .../TestStream_v3_4097.cfs | Bin .../TestStream_v3_511.cfs | Bin .../TestStream_v3_512.cfs | Bin .../TestStream_v3_513.cfs | Bin .../TestStream_v3_63.cfs | Bin .../TestStream_v3_64.cfs | Bin .../TestStream_v3_65.cfs | Bin .../TestStream_v3_65536.cfs | Bin .../TestStream_v4_0.cfs | Bin .../TestStream_v4_4095.cfs | Bin .../TestStream_v4_4096.cfs | Bin .../TestStream_v4_4097.cfs | Bin .../TestStream_v4_511.cfs | Bin .../TestStream_v4_512.cfs | Bin .../TestStream_v4_513.cfs | Bin .../TestStream_v4_63.cfs | Bin .../TestStream_v4_64.cfs | Bin .../TestStream_v4_65.cfs | Bin OpenMcdf3.sln => OpenMcdf.sln | 8 ++++---- {OpenMcdf3 => OpenMcdf}/AssemblyInfo.cs | 2 +- {OpenMcdf3 => OpenMcdf}/CfbBinaryReader.cs | 2 +- {OpenMcdf3 => OpenMcdf}/CfbBinaryWriter.cs | 2 +- {OpenMcdf3 => OpenMcdf}/CfbStream.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DifatSectorEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryEntries.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryEntry.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryEntryComparer.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryEntryEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryTree.cs | 2 +- {OpenMcdf3 => OpenMcdf}/DirectoryTreeEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/EntryInfo.cs | 2 +- {OpenMcdf3 => OpenMcdf}/Fat.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatChainEntry.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatChainEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatEntry.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatSectorEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/FatStream.cs | 2 +- {OpenMcdf3 => OpenMcdf}/Header.cs | 2 +- {OpenMcdf3 => OpenMcdf}/IOContext.cs | 2 +- {OpenMcdf3 => OpenMcdf}/MiniFat.cs | 2 +- {OpenMcdf3 => OpenMcdf}/MiniFatChainEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/MiniFatEnumerator.cs | 2 +- {OpenMcdf3 => OpenMcdf}/MiniFatStream.cs | 2 +- {OpenMcdf3 => OpenMcdf}/MiniSector.cs | 2 +- .../OpenMcdf3.csproj => OpenMcdf/OpenMcdf.csproj | 2 ++ {OpenMcdf3 => OpenMcdf}/RootStorage.cs | 2 +- {OpenMcdf3 => OpenMcdf}/Sector.cs | 2 +- {OpenMcdf3 => OpenMcdf}/SectorDataCache.cs | 2 +- {OpenMcdf3 => OpenMcdf}/SectorType.cs | 2 +- {OpenMcdf3 => OpenMcdf}/Storage.cs | 2 +- {OpenMcdf3 => OpenMcdf}/StreamExtensions.cs | 2 +- {OpenMcdf3 => OpenMcdf}/System/.editorconfig | 0 {OpenMcdf3 => OpenMcdf}/System/.globalconfig | 0 {OpenMcdf3 => OpenMcdf}/System/CompilerServices.cs | 0 {OpenMcdf3 => OpenMcdf}/System/Index.cs | 0 .../System/NullableAttributes.cs | 0 {OpenMcdf3 => OpenMcdf}/System/Range.cs | 0 {OpenMcdf3 => OpenMcdf}/ThrowHelper.cs | 2 +- {OpenMcdf3 => OpenMcdf}/TransactedStream.cs | 2 +- StructuredStorageExplorer/MainForm.cs | 2 +- StructuredStorageExplorer/StreamDataProvider.cs | 2 +- .../StructuredStorageExplorer.csproj | 2 +- 93 files changed, 74 insertions(+), 69 deletions(-) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/FileStreamRead.cs (95%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/FileStreamTransactedWrite.cs (95%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/FileStreamWrite.cs (95%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/MemoryStreamRead.cs (95%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/MemoryStreamTransactedWrite.cs (94%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/MemoryStreamWrite.cs (95%) rename OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj => OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj (88%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/OpenMcdfBenchmarks.cs (97%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/Program.cs (86%) rename {OpenMcdf3.Benchmarks => OpenMcdf.Benchmarks}/StructuredStorageBenchmarks.cs (98%) rename OpenMcdf3.Perf/OpenMcdf3.Perf.csproj => OpenMcdf.Perf/OpenMcdf.Perf.csproj (80%) rename {OpenMcdf3.Perf => OpenMcdf.Perf}/Program.cs (98%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/AssemblyInfo.cs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/BinaryReaderTests.cs (97%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/BinaryWriterTests.cs (98%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/DebugWriter.cs (94%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/EntryInfoTests.cs (93%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/MultipleStorage.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/MultipleStorage2.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/MultipleStorage3.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/MultipleStorage4.cfs (100%) rename OpenMcdf3.Tests/OpenMcdf3.Tests.csproj => OpenMcdf.Tests/OpenMcdf.Tests.csproj (98%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/StorageTests.cs (99%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/StreamAssert.cs (96%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/StreamTests.cs (99%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_0.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_4095.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_4096.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_4097.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_511.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_512.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_513.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_63.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_64.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_65.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v3_65536.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_0.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_4095.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_4096.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_4097.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_511.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_512.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_513.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_63.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_64.cfs (100%) rename {OpenMcdf3.Tests => OpenMcdf.Tests}/TestStream_v4_65.cfs (100%) rename OpenMcdf3.sln => OpenMcdf.sln (86%) rename {OpenMcdf3 => OpenMcdf}/AssemblyInfo.cs (94%) rename {OpenMcdf3 => OpenMcdf}/CfbBinaryReader.cs (99%) rename {OpenMcdf3 => OpenMcdf}/CfbBinaryWriter.cs (99%) rename {OpenMcdf3 => OpenMcdf}/CfbStream.cs (99%) rename {OpenMcdf3 => OpenMcdf}/DifatSectorEnumerator.cs (99%) rename {OpenMcdf3 => OpenMcdf}/DirectoryEntries.cs (99%) rename {OpenMcdf3 => OpenMcdf}/DirectoryEntry.cs (99%) rename {OpenMcdf3 => OpenMcdf}/DirectoryEntryComparer.cs (97%) rename {OpenMcdf3 => OpenMcdf}/DirectoryEntryEnumerator.cs (98%) rename {OpenMcdf3 => OpenMcdf}/DirectoryTree.cs (99%) rename {OpenMcdf3 => OpenMcdf}/DirectoryTreeEnumerator.cs (98%) rename {OpenMcdf3 => OpenMcdf}/EntryInfo.cs (93%) rename {OpenMcdf3 => OpenMcdf}/Fat.cs (99%) rename {OpenMcdf3 => OpenMcdf}/FatChainEntry.cs (93%) rename {OpenMcdf3 => OpenMcdf}/FatChainEnumerator.cs (99%) rename {OpenMcdf3 => OpenMcdf}/FatEntry.cs (93%) rename {OpenMcdf3 => OpenMcdf}/FatEnumerator.cs (98%) rename {OpenMcdf3 => OpenMcdf}/FatSectorEnumerator.cs (99%) rename {OpenMcdf3 => OpenMcdf}/FatStream.cs (99%) rename {OpenMcdf3 => OpenMcdf}/Header.cs (99%) rename {OpenMcdf3 => OpenMcdf}/IOContext.cs (99%) rename {OpenMcdf3 => OpenMcdf}/MiniFat.cs (99%) rename {OpenMcdf3 => OpenMcdf}/MiniFatChainEnumerator.cs (99%) rename {OpenMcdf3 => OpenMcdf}/MiniFatEnumerator.cs (99%) rename {OpenMcdf3 => OpenMcdf}/MiniFatStream.cs (99%) rename {OpenMcdf3 => OpenMcdf}/MiniSector.cs (98%) rename OpenMcdf3/OpenMcdf3.csproj => OpenMcdf/OpenMcdf.csproj (90%) rename {OpenMcdf3 => OpenMcdf}/RootStorage.cs (99%) rename {OpenMcdf3 => OpenMcdf}/Sector.cs (98%) rename {OpenMcdf3 => OpenMcdf}/SectorDataCache.cs (97%) rename {OpenMcdf3 => OpenMcdf}/SectorType.cs (95%) rename {OpenMcdf3 => OpenMcdf}/Storage.cs (99%) rename {OpenMcdf3 => OpenMcdf}/StreamExtensions.cs (98%) rename {OpenMcdf3 => OpenMcdf}/System/.editorconfig (100%) rename {OpenMcdf3 => OpenMcdf}/System/.globalconfig (100%) rename {OpenMcdf3 => OpenMcdf}/System/CompilerServices.cs (100%) rename {OpenMcdf3 => OpenMcdf}/System/Index.cs (100%) rename {OpenMcdf3 => OpenMcdf}/System/NullableAttributes.cs (100%) rename {OpenMcdf3 => OpenMcdf}/System/Range.cs (100%) rename {OpenMcdf3 => OpenMcdf}/ThrowHelper.cs (99%) rename {OpenMcdf3 => OpenMcdf}/TransactedStream.cs (99%) diff --git a/OpenMcdf3.Benchmarks/FileStreamRead.cs b/OpenMcdf.Benchmarks/FileStreamRead.cs similarity index 95% rename from OpenMcdf3.Benchmarks/FileStreamRead.cs rename to OpenMcdf.Benchmarks/FileStreamRead.cs index ed3b3e1d..3dd43341 100644 --- a/OpenMcdf3.Benchmarks/FileStreamRead.cs +++ b/OpenMcdf.Benchmarks/FileStreamRead.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [MediumRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs b/OpenMcdf.Benchmarks/FileStreamTransactedWrite.cs similarity index 95% rename from OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs rename to OpenMcdf.Benchmarks/FileStreamTransactedWrite.cs index 3f7b1a3d..ff5b5ddf 100644 --- a/OpenMcdf3.Benchmarks/FileStreamTransactedWrite.cs +++ b/OpenMcdf.Benchmarks/FileStreamTransactedWrite.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [MediumRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/FileStreamWrite.cs b/OpenMcdf.Benchmarks/FileStreamWrite.cs similarity index 95% rename from OpenMcdf3.Benchmarks/FileStreamWrite.cs rename to OpenMcdf.Benchmarks/FileStreamWrite.cs index d5b0d8ce..afc7c12b 100644 --- a/OpenMcdf3.Benchmarks/FileStreamWrite.cs +++ b/OpenMcdf.Benchmarks/FileStreamWrite.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [MediumRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/MemoryStreamRead.cs b/OpenMcdf.Benchmarks/MemoryStreamRead.cs similarity index 95% rename from OpenMcdf3.Benchmarks/MemoryStreamRead.cs rename to OpenMcdf.Benchmarks/MemoryStreamRead.cs index 56a40285..8d48a54a 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamRead.cs +++ b/OpenMcdf.Benchmarks/MemoryStreamRead.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [ShortRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs b/OpenMcdf.Benchmarks/MemoryStreamTransactedWrite.cs similarity index 94% rename from OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs rename to OpenMcdf.Benchmarks/MemoryStreamTransactedWrite.cs index 73c6ef87..46783fc6 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamTransactedWrite.cs +++ b/OpenMcdf.Benchmarks/MemoryStreamTransactedWrite.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [ShortRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs b/OpenMcdf.Benchmarks/MemoryStreamWrite.cs similarity index 95% rename from OpenMcdf3.Benchmarks/MemoryStreamWrite.cs rename to OpenMcdf.Benchmarks/MemoryStreamWrite.cs index db669337..4935822e 100644 --- a/OpenMcdf3.Benchmarks/MemoryStreamWrite.cs +++ b/OpenMcdf.Benchmarks/MemoryStreamWrite.cs @@ -1,8 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; -using OpenMcdf3.Benchmarks; +using OpenMcdf.Benchmarks; -namespace OpenMcdf3.Benchmark; +namespace OpenMcdf.Benchmark; [ShortRunJob] [MemoryDiagnoser] diff --git a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj b/OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj similarity index 88% rename from OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj rename to OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj index 219e3058..cdcc1431 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdf3.Benchmarks.csproj +++ b/OpenMcdf.Benchmarks/OpenMcdf.Benchmarks.csproj @@ -13,7 +13,7 @@ - + diff --git a/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs b/OpenMcdf.Benchmarks/OpenMcdfBenchmarks.cs similarity index 97% rename from OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs rename to OpenMcdf.Benchmarks/OpenMcdfBenchmarks.cs index f4232b5c..e2486bcc 100644 --- a/OpenMcdf3.Benchmarks/OpenMcdfBenchmarks.cs +++ b/OpenMcdf.Benchmarks/OpenMcdfBenchmarks.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3.Benchmarks; +namespace OpenMcdf.Benchmarks; internal static class OpenMcdfBenchmarks { diff --git a/OpenMcdf3.Benchmarks/Program.cs b/OpenMcdf.Benchmarks/Program.cs similarity index 86% rename from OpenMcdf3.Benchmarks/Program.cs rename to OpenMcdf.Benchmarks/Program.cs index 05756ebc..2dd439f8 100644 --- a/OpenMcdf3.Benchmarks/Program.cs +++ b/OpenMcdf.Benchmarks/Program.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Running; -namespace OpenMcdf3.Benchmarks; +namespace OpenMcdf.Benchmarks; internal sealed class Program { diff --git a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs b/OpenMcdf.Benchmarks/StructuredStorageBenchmarks.cs similarity index 98% rename from OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs rename to OpenMcdf.Benchmarks/StructuredStorageBenchmarks.cs index eda2ffd3..c34938ca 100644 --- a/OpenMcdf3.Benchmarks/StructuredStorageBenchmarks.cs +++ b/OpenMcdf.Benchmarks/StructuredStorageBenchmarks.cs @@ -1,6 +1,6 @@ using StructuredStorage; -namespace OpenMcdf3.Benchmarks; +namespace OpenMcdf.Benchmarks; internal static class StructuredStorageBenchmarks { diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index 8ff3f80f..e47220f4 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -1,4 +1,4 @@ -using OpenMcdf3; +using OpenMcdf; namespace OpenMcdf.Ole; diff --git a/OpenMcdf.Ole/OpenMcdf.Ole.csproj b/OpenMcdf.Ole/OpenMcdf.Ole.csproj index 101f0831..12de727d 100644 --- a/OpenMcdf.Ole/OpenMcdf.Ole.csproj +++ b/OpenMcdf.Ole/OpenMcdf.Ole.csproj @@ -5,12 +5,13 @@ enable enable + - + \ No newline at end of file diff --git a/OpenMcdf3.Perf/OpenMcdf3.Perf.csproj b/OpenMcdf.Perf/OpenMcdf.Perf.csproj similarity index 80% rename from OpenMcdf3.Perf/OpenMcdf3.Perf.csproj rename to OpenMcdf.Perf/OpenMcdf.Perf.csproj index 9d82e145..e41e919c 100644 --- a/OpenMcdf3.Perf/OpenMcdf3.Perf.csproj +++ b/OpenMcdf.Perf/OpenMcdf.Perf.csproj @@ -8,7 +8,7 @@ - + diff --git a/OpenMcdf3.Perf/Program.cs b/OpenMcdf.Perf/Program.cs similarity index 98% rename from OpenMcdf3.Perf/Program.cs rename to OpenMcdf.Perf/Program.cs index 751cf9b1..41a62486 100644 --- a/OpenMcdf3.Perf/Program.cs +++ b/OpenMcdf.Perf/Program.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace OpenMcdf3.Perf; +namespace OpenMcdf.Perf; internal sealed class Program { diff --git a/OpenMcdf3.Tests/AssemblyInfo.cs b/OpenMcdf.Tests/AssemblyInfo.cs similarity index 100% rename from OpenMcdf3.Tests/AssemblyInfo.cs rename to OpenMcdf.Tests/AssemblyInfo.cs diff --git a/OpenMcdf3.Tests/BinaryReaderTests.cs b/OpenMcdf.Tests/BinaryReaderTests.cs similarity index 97% rename from OpenMcdf3.Tests/BinaryReaderTests.cs rename to OpenMcdf.Tests/BinaryReaderTests.cs index e5bfa095..9dc8bb34 100644 --- a/OpenMcdf3.Tests/BinaryReaderTests.cs +++ b/OpenMcdf.Tests/BinaryReaderTests.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; [TestClass] public sealed class BinaryReaderTests diff --git a/OpenMcdf3.Tests/BinaryWriterTests.cs b/OpenMcdf.Tests/BinaryWriterTests.cs similarity index 98% rename from OpenMcdf3.Tests/BinaryWriterTests.cs rename to OpenMcdf.Tests/BinaryWriterTests.cs index cd45cff0..2a245715 100644 --- a/OpenMcdf3.Tests/BinaryWriterTests.cs +++ b/OpenMcdf.Tests/BinaryWriterTests.cs @@ -1,6 +1,6 @@ using System.Text; -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; [TestClass] public sealed class BinaryWriterTests diff --git a/OpenMcdf3.Tests/DebugWriter.cs b/OpenMcdf.Tests/DebugWriter.cs similarity index 94% rename from OpenMcdf3.Tests/DebugWriter.cs rename to OpenMcdf.Tests/DebugWriter.cs index c1b66277..24762f30 100644 --- a/OpenMcdf3.Tests/DebugWriter.cs +++ b/OpenMcdf.Tests/DebugWriter.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Text; -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; internal sealed class DebugWriter : TextWriter { diff --git a/OpenMcdf3.Tests/EntryInfoTests.cs b/OpenMcdf.Tests/EntryInfoTests.cs similarity index 93% rename from OpenMcdf3.Tests/EntryInfoTests.cs rename to OpenMcdf.Tests/EntryInfoTests.cs index 60d6a320..b431a7ed 100644 --- a/OpenMcdf3.Tests/EntryInfoTests.cs +++ b/OpenMcdf.Tests/EntryInfoTests.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; [TestClass] public sealed class EntryInfoTests diff --git a/OpenMcdf3.Tests/MultipleStorage.cfs b/OpenMcdf.Tests/MultipleStorage.cfs similarity index 100% rename from OpenMcdf3.Tests/MultipleStorage.cfs rename to OpenMcdf.Tests/MultipleStorage.cfs diff --git a/OpenMcdf3.Tests/MultipleStorage2.cfs b/OpenMcdf.Tests/MultipleStorage2.cfs similarity index 100% rename from OpenMcdf3.Tests/MultipleStorage2.cfs rename to OpenMcdf.Tests/MultipleStorage2.cfs diff --git a/OpenMcdf3.Tests/MultipleStorage3.cfs b/OpenMcdf.Tests/MultipleStorage3.cfs similarity index 100% rename from OpenMcdf3.Tests/MultipleStorage3.cfs rename to OpenMcdf.Tests/MultipleStorage3.cfs diff --git a/OpenMcdf3.Tests/MultipleStorage4.cfs b/OpenMcdf.Tests/MultipleStorage4.cfs similarity index 100% rename from OpenMcdf3.Tests/MultipleStorage4.cfs rename to OpenMcdf.Tests/MultipleStorage4.cfs diff --git a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj b/OpenMcdf.Tests/OpenMcdf.Tests.csproj similarity index 98% rename from OpenMcdf3.Tests/OpenMcdf3.Tests.csproj rename to OpenMcdf.Tests/OpenMcdf.Tests.csproj index 7b0ea833..2736a5b8 100644 --- a/OpenMcdf3.Tests/OpenMcdf3.Tests.csproj +++ b/OpenMcdf.Tests/OpenMcdf.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/OpenMcdf3.Tests/StorageTests.cs b/OpenMcdf.Tests/StorageTests.cs similarity index 99% rename from OpenMcdf3.Tests/StorageTests.cs rename to OpenMcdf.Tests/StorageTests.cs index de979cf4..1043e93c 100644 --- a/OpenMcdf3.Tests/StorageTests.cs +++ b/OpenMcdf.Tests/StorageTests.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; + +using Version = OpenMcdf.Version; [TestClass] public sealed class StorageTests diff --git a/OpenMcdf3.Tests/StreamAssert.cs b/OpenMcdf.Tests/StreamAssert.cs similarity index 96% rename from OpenMcdf3.Tests/StreamAssert.cs rename to OpenMcdf.Tests/StreamAssert.cs index 328cbe86..4d8c8c07 100644 --- a/OpenMcdf3.Tests/StreamAssert.cs +++ b/OpenMcdf.Tests/StreamAssert.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; internal static class StreamAssert { diff --git a/OpenMcdf3.Tests/StreamTests.cs b/OpenMcdf.Tests/StreamTests.cs similarity index 99% rename from OpenMcdf3.Tests/StreamTests.cs rename to OpenMcdf.Tests/StreamTests.cs index d9237544..0401e7c9 100644 --- a/OpenMcdf3.Tests/StreamTests.cs +++ b/OpenMcdf.Tests/StreamTests.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3.Tests; +namespace OpenMcdf.Tests; [TestClass] public sealed class StreamTests diff --git a/OpenMcdf3.Tests/TestStream_v3_0.cfs b/OpenMcdf.Tests/TestStream_v3_0.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_0.cfs rename to OpenMcdf.Tests/TestStream_v3_0.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_4095.cfs b/OpenMcdf.Tests/TestStream_v3_4095.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_4095.cfs rename to OpenMcdf.Tests/TestStream_v3_4095.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_4096.cfs b/OpenMcdf.Tests/TestStream_v3_4096.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_4096.cfs rename to OpenMcdf.Tests/TestStream_v3_4096.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_4097.cfs b/OpenMcdf.Tests/TestStream_v3_4097.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_4097.cfs rename to OpenMcdf.Tests/TestStream_v3_4097.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_511.cfs b/OpenMcdf.Tests/TestStream_v3_511.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_511.cfs rename to OpenMcdf.Tests/TestStream_v3_511.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_512.cfs b/OpenMcdf.Tests/TestStream_v3_512.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_512.cfs rename to OpenMcdf.Tests/TestStream_v3_512.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_513.cfs b/OpenMcdf.Tests/TestStream_v3_513.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_513.cfs rename to OpenMcdf.Tests/TestStream_v3_513.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_63.cfs b/OpenMcdf.Tests/TestStream_v3_63.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_63.cfs rename to OpenMcdf.Tests/TestStream_v3_63.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_64.cfs b/OpenMcdf.Tests/TestStream_v3_64.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_64.cfs rename to OpenMcdf.Tests/TestStream_v3_64.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_65.cfs b/OpenMcdf.Tests/TestStream_v3_65.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_65.cfs rename to OpenMcdf.Tests/TestStream_v3_65.cfs diff --git a/OpenMcdf3.Tests/TestStream_v3_65536.cfs b/OpenMcdf.Tests/TestStream_v3_65536.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v3_65536.cfs rename to OpenMcdf.Tests/TestStream_v3_65536.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_0.cfs b/OpenMcdf.Tests/TestStream_v4_0.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_0.cfs rename to OpenMcdf.Tests/TestStream_v4_0.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_4095.cfs b/OpenMcdf.Tests/TestStream_v4_4095.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_4095.cfs rename to OpenMcdf.Tests/TestStream_v4_4095.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_4096.cfs b/OpenMcdf.Tests/TestStream_v4_4096.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_4096.cfs rename to OpenMcdf.Tests/TestStream_v4_4096.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_4097.cfs b/OpenMcdf.Tests/TestStream_v4_4097.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_4097.cfs rename to OpenMcdf.Tests/TestStream_v4_4097.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_511.cfs b/OpenMcdf.Tests/TestStream_v4_511.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_511.cfs rename to OpenMcdf.Tests/TestStream_v4_511.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_512.cfs b/OpenMcdf.Tests/TestStream_v4_512.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_512.cfs rename to OpenMcdf.Tests/TestStream_v4_512.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_513.cfs b/OpenMcdf.Tests/TestStream_v4_513.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_513.cfs rename to OpenMcdf.Tests/TestStream_v4_513.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_63.cfs b/OpenMcdf.Tests/TestStream_v4_63.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_63.cfs rename to OpenMcdf.Tests/TestStream_v4_63.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_64.cfs b/OpenMcdf.Tests/TestStream_v4_64.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_64.cfs rename to OpenMcdf.Tests/TestStream_v4_64.cfs diff --git a/OpenMcdf3.Tests/TestStream_v4_65.cfs b/OpenMcdf.Tests/TestStream_v4_65.cfs similarity index 100% rename from OpenMcdf3.Tests/TestStream_v4_65.cfs rename to OpenMcdf.Tests/TestStream_v4_65.cfs diff --git a/OpenMcdf3.sln b/OpenMcdf.sln similarity index 86% rename from OpenMcdf3.sln rename to OpenMcdf.sln index 38cdc0f8..0e7f8a06 100644 --- a/OpenMcdf3.sln +++ b/OpenMcdf.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3", "OpenMcdf3\OpenMcdf3.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf", "OpenMcdf\OpenMcdf.csproj", "{B90DDE7E-803A-4890-82F0-09DAD0FF66D8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Tests", "OpenMcdf3.Tests\OpenMcdf3.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Tests", "OpenMcdf.Tests\OpenMcdf.Tests.csproj", "{96A9DA9C-E4C2-4531-A2E4-154F1FBF7532}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34030FA7-0A06-43D1-85DD-ADD39D502C3C}" ProjectSection(SolutionItems) = preProject @@ -14,9 +14,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution exclusion.dic = exclusion.dic EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Benchmarks", "OpenMcdf3.Benchmarks\OpenMcdf3.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Benchmarks", "OpenMcdf.Benchmarks\OpenMcdf.Benchmarks.csproj", "{44C718AD-F7FE-4733-80A8-636E5E7E63F3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf3.Perf", "OpenMcdf3.Perf\OpenMcdf3.Perf.csproj", "{8167F453-A244-4FE2-9B33-A7B80B1B7AB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Perf", "OpenMcdf.Perf\OpenMcdf.Perf.csproj", "{8167F453-A244-4FE2-9B33-A7B80B1B7AB1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorage", "StructuredStorage\StructuredStorage.csproj", "{D7861D73-B42C-403E-9B9E-F921BC70F0D3}" EndProject diff --git a/OpenMcdf3/AssemblyInfo.cs b/OpenMcdf/AssemblyInfo.cs similarity index 94% rename from OpenMcdf3/AssemblyInfo.cs rename to OpenMcdf/AssemblyInfo.cs index b7349b84..3905293e 100644 --- a/OpenMcdf3/AssemblyInfo.cs +++ b/OpenMcdf/AssemblyInfo.cs @@ -16,4 +16,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM. [assembly: Guid("a96ebb34-8c16-4c7e-b9f7-651ba754b722")] -[assembly: InternalsVisibleTo("OpenMcdf3.Tests")] +[assembly: InternalsVisibleTo("OpenMcdf.Tests")] diff --git a/OpenMcdf3/CfbBinaryReader.cs b/OpenMcdf/CfbBinaryReader.cs similarity index 99% rename from OpenMcdf3/CfbBinaryReader.cs rename to OpenMcdf/CfbBinaryReader.cs index 2d1e00a2..371ca7db 100644 --- a/OpenMcdf3/CfbBinaryReader.cs +++ b/OpenMcdf/CfbBinaryReader.cs @@ -5,7 +5,7 @@ using System.Buffers; #endif -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Reads CFB data types from a stream. diff --git a/OpenMcdf3/CfbBinaryWriter.cs b/OpenMcdf/CfbBinaryWriter.cs similarity index 99% rename from OpenMcdf3/CfbBinaryWriter.cs rename to OpenMcdf/CfbBinaryWriter.cs index 6b4f9ba4..ec18ed11 100644 --- a/OpenMcdf3/CfbBinaryWriter.cs +++ b/OpenMcdf/CfbBinaryWriter.cs @@ -1,6 +1,6 @@ using System.Text; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Writes CFB data types to a stream. diff --git a/OpenMcdf3/CfbStream.cs b/OpenMcdf/CfbStream.cs similarity index 99% rename from OpenMcdf3/CfbStream.cs rename to OpenMcdf/CfbStream.cs index 5d076bfd..a905f177 100644 --- a/OpenMcdf3/CfbStream.cs +++ b/OpenMcdf/CfbStream.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Represents a stream in a compound file. diff --git a/OpenMcdf3/DifatSectorEnumerator.cs b/OpenMcdf/DifatSectorEnumerator.cs similarity index 99% rename from OpenMcdf3/DifatSectorEnumerator.cs rename to OpenMcdf/DifatSectorEnumerator.cs index 4e306901..a2c3589e 100644 --- a/OpenMcdf3/DifatSectorEnumerator.cs +++ b/OpenMcdf/DifatSectorEnumerator.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; internal class DifatSectorEnumerator : IEnumerator { diff --git a/OpenMcdf3/DirectoryEntries.cs b/OpenMcdf/DirectoryEntries.cs similarity index 99% rename from OpenMcdf3/DirectoryEntries.cs rename to OpenMcdf/DirectoryEntries.cs index e217737b..a6163fec 100644 --- a/OpenMcdf3/DirectoryEntries.cs +++ b/OpenMcdf/DirectoryEntries.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; internal sealed class DirectoryEntries : IDisposable { diff --git a/OpenMcdf3/DirectoryEntry.cs b/OpenMcdf/DirectoryEntry.cs similarity index 99% rename from OpenMcdf3/DirectoryEntry.cs rename to OpenMcdf/DirectoryEntry.cs index 8b9b36f1..6eac4af8 100644 --- a/OpenMcdf3/DirectoryEntry.cs +++ b/OpenMcdf/DirectoryEntry.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// The storage type of a . diff --git a/OpenMcdf3/DirectoryEntryComparer.cs b/OpenMcdf/DirectoryEntryComparer.cs similarity index 97% rename from OpenMcdf3/DirectoryEntryComparer.cs rename to OpenMcdf/DirectoryEntryComparer.cs index f4799b15..ae255374 100644 --- a/OpenMcdf3/DirectoryEntryComparer.cs +++ b/OpenMcdf/DirectoryEntryComparer.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; internal class DirectoryEntryComparer : IComparer { diff --git a/OpenMcdf3/DirectoryEntryEnumerator.cs b/OpenMcdf/DirectoryEntryEnumerator.cs similarity index 98% rename from OpenMcdf3/DirectoryEntryEnumerator.cs rename to OpenMcdf/DirectoryEntryEnumerator.cs index 1e73a062..f80f78d1 100644 --- a/OpenMcdf3/DirectoryEntryEnumerator.cs +++ b/OpenMcdf/DirectoryEntryEnumerator.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates instances from a . diff --git a/OpenMcdf3/DirectoryTree.cs b/OpenMcdf/DirectoryTree.cs similarity index 99% rename from OpenMcdf3/DirectoryTree.cs rename to OpenMcdf/DirectoryTree.cs index f3cfccec..65b69ca5 100644 --- a/OpenMcdf3/DirectoryTree.cs +++ b/OpenMcdf/DirectoryTree.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; internal class DirectoryTree { diff --git a/OpenMcdf3/DirectoryTreeEnumerator.cs b/OpenMcdf/DirectoryTreeEnumerator.cs similarity index 98% rename from OpenMcdf3/DirectoryTreeEnumerator.cs rename to OpenMcdf/DirectoryTreeEnumerator.cs index 9b1eae0c..6bbd1e50 100644 --- a/OpenMcdf3/DirectoryTreeEnumerator.cs +++ b/OpenMcdf/DirectoryTreeEnumerator.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the children of a . diff --git a/OpenMcdf3/EntryInfo.cs b/OpenMcdf/EntryInfo.cs similarity index 93% rename from OpenMcdf3/EntryInfo.cs rename to OpenMcdf/EntryInfo.cs index c3311498..d88809d4 100644 --- a/OpenMcdf3/EntryInfo.cs +++ b/OpenMcdf/EntryInfo.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; public enum EntryType { diff --git a/OpenMcdf3/Fat.cs b/OpenMcdf/Fat.cs similarity index 99% rename from OpenMcdf3/Fat.cs rename to OpenMcdf/Fat.cs index 9e3a2b0a..30d75e39 100644 --- a/OpenMcdf3/Fat.cs +++ b/OpenMcdf/Fat.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Encapsulates getting and setting entries in the FAT. diff --git a/OpenMcdf3/FatChainEntry.cs b/OpenMcdf/FatChainEntry.cs similarity index 93% rename from OpenMcdf3/FatChainEntry.cs rename to OpenMcdf/FatChainEntry.cs index ba7302fc..0f5fdde8 100644 --- a/OpenMcdf3/FatChainEntry.cs +++ b/OpenMcdf/FatChainEntry.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; internal record struct FatChainEntry(uint Index, uint Value) { diff --git a/OpenMcdf3/FatChainEnumerator.cs b/OpenMcdf/FatChainEnumerator.cs similarity index 99% rename from OpenMcdf3/FatChainEnumerator.cs rename to OpenMcdf/FatChainEnumerator.cs index beb09245..300fa0b3 100644 --- a/OpenMcdf3/FatChainEnumerator.cs +++ b/OpenMcdf/FatChainEnumerator.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the s in a FAT sector chain. diff --git a/OpenMcdf3/FatEntry.cs b/OpenMcdf/FatEntry.cs similarity index 93% rename from OpenMcdf3/FatEntry.cs rename to OpenMcdf/FatEntry.cs index db01412c..915eb85e 100644 --- a/OpenMcdf3/FatEntry.cs +++ b/OpenMcdf/FatEntry.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Encapsulates an entry in the File Allocation Table (FAT). diff --git a/OpenMcdf3/FatEnumerator.cs b/OpenMcdf/FatEnumerator.cs similarity index 98% rename from OpenMcdf3/FatEnumerator.cs rename to OpenMcdf/FatEnumerator.cs index ee59bbb3..4d8eed07 100644 --- a/OpenMcdf3/FatEnumerator.cs +++ b/OpenMcdf/FatEnumerator.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the entries in a FAT. diff --git a/OpenMcdf3/FatSectorEnumerator.cs b/OpenMcdf/FatSectorEnumerator.cs similarity index 99% rename from OpenMcdf3/FatSectorEnumerator.cs rename to OpenMcdf/FatSectorEnumerator.cs index d4464514..1381dc9e 100644 --- a/OpenMcdf3/FatSectorEnumerator.cs +++ b/OpenMcdf/FatSectorEnumerator.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the FAT sectors of a compound file. diff --git a/OpenMcdf3/FatStream.cs b/OpenMcdf/FatStream.cs similarity index 99% rename from OpenMcdf3/FatStream.cs rename to OpenMcdf/FatStream.cs index bc0dd9a4..d3ec8f59 100644 --- a/OpenMcdf3/FatStream.cs +++ b/OpenMcdf/FatStream.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Provides a for a stream object in a compound file./> diff --git a/OpenMcdf3/Header.cs b/OpenMcdf/Header.cs similarity index 99% rename from OpenMcdf3/Header.cs rename to OpenMcdf/Header.cs index 900a2936..eb277c70 100644 --- a/OpenMcdf3/Header.cs +++ b/OpenMcdf/Header.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// The structure at the beginning of a compound file. diff --git a/OpenMcdf3/IOContext.cs b/OpenMcdf/IOContext.cs similarity index 99% rename from OpenMcdf3/IOContext.cs rename to OpenMcdf/IOContext.cs index 7966c611..da0cefd4 100644 --- a/OpenMcdf3/IOContext.cs +++ b/OpenMcdf/IOContext.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; enum IOContextFlags { diff --git a/OpenMcdf3/MiniFat.cs b/OpenMcdf/MiniFat.cs similarity index 99% rename from OpenMcdf3/MiniFat.cs rename to OpenMcdf/MiniFat.cs index bbb22d6b..1ed7bc71 100644 --- a/OpenMcdf3/MiniFat.cs +++ b/OpenMcdf/MiniFat.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Encapsulates getting and setting entries in the mini FAT. diff --git a/OpenMcdf3/MiniFatChainEnumerator.cs b/OpenMcdf/MiniFatChainEnumerator.cs similarity index 99% rename from OpenMcdf3/MiniFatChainEnumerator.cs rename to OpenMcdf/MiniFatChainEnumerator.cs index e261243f..1b02e56b 100644 --- a/OpenMcdf3/MiniFatChainEnumerator.cs +++ b/OpenMcdf/MiniFatChainEnumerator.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the s in a Mini FAT sector chain. diff --git a/OpenMcdf3/MiniFatEnumerator.cs b/OpenMcdf/MiniFatEnumerator.cs similarity index 99% rename from OpenMcdf3/MiniFatEnumerator.cs rename to OpenMcdf/MiniFatEnumerator.cs index 4d108a1d..d7cd55a0 100644 --- a/OpenMcdf3/MiniFatEnumerator.cs +++ b/OpenMcdf/MiniFatEnumerator.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Enumerates the s from the Mini FAT. diff --git a/OpenMcdf3/MiniFatStream.cs b/OpenMcdf/MiniFatStream.cs similarity index 99% rename from OpenMcdf3/MiniFatStream.cs rename to OpenMcdf/MiniFatStream.cs index b3a1beea..6afaf9f5 100644 --- a/OpenMcdf3/MiniFatStream.cs +++ b/OpenMcdf/MiniFatStream.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Provides a for reading a from the mini FAT stream. diff --git a/OpenMcdf3/MiniSector.cs b/OpenMcdf/MiniSector.cs similarity index 98% rename from OpenMcdf3/MiniSector.cs rename to OpenMcdf/MiniSector.cs index daf5b16e..e20f1340 100644 --- a/OpenMcdf3/MiniSector.cs +++ b/OpenMcdf/MiniSector.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Encapsulates information about a mini sector in a compound file. diff --git a/OpenMcdf3/OpenMcdf3.csproj b/OpenMcdf/OpenMcdf.csproj similarity index 90% rename from OpenMcdf3/OpenMcdf3.csproj rename to OpenMcdf/OpenMcdf.csproj index 884c3d20..7a2d5751 100644 --- a/OpenMcdf3/OpenMcdf3.csproj +++ b/OpenMcdf/OpenMcdf.csproj @@ -6,6 +6,8 @@ enable enable true + 3.0.0.0 + ironfede,Jeremy Powell diff --git a/OpenMcdf3/RootStorage.cs b/OpenMcdf/RootStorage.cs similarity index 99% rename from OpenMcdf3/RootStorage.cs rename to OpenMcdf/RootStorage.cs index b50fb065..1120a8a4 100644 --- a/OpenMcdf3/RootStorage.cs +++ b/OpenMcdf/RootStorage.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; public enum Version : ushort { diff --git a/OpenMcdf3/Sector.cs b/OpenMcdf/Sector.cs similarity index 98% rename from OpenMcdf3/Sector.cs rename to OpenMcdf/Sector.cs index 07beb12d..d7d36573 100644 --- a/OpenMcdf3/Sector.cs +++ b/OpenMcdf/Sector.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Encapsulates information about a sector in a compound file. diff --git a/OpenMcdf3/SectorDataCache.cs b/OpenMcdf/SectorDataCache.cs similarity index 97% rename from OpenMcdf3/SectorDataCache.cs rename to OpenMcdf/SectorDataCache.cs index 77085b56..8c04e001 100644 --- a/OpenMcdf3/SectorDataCache.cs +++ b/OpenMcdf/SectorDataCache.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using System.Runtime.InteropServices; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Caches data for adding new sectors to the FAT. diff --git a/OpenMcdf3/SectorType.cs b/OpenMcdf/SectorType.cs similarity index 95% rename from OpenMcdf3/SectorType.cs rename to OpenMcdf/SectorType.cs index 83582262..7735f962 100644 --- a/OpenMcdf3/SectorType.cs +++ b/OpenMcdf/SectorType.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Defines the types of sectors in a compound file. diff --git a/OpenMcdf3/Storage.cs b/OpenMcdf/Storage.cs similarity index 99% rename from OpenMcdf3/Storage.cs rename to OpenMcdf/Storage.cs index fa3f9722..27b01609 100644 --- a/OpenMcdf3/Storage.cs +++ b/OpenMcdf/Storage.cs @@ -1,4 +1,4 @@ -namespace OpenMcdf3; +namespace OpenMcdf; /// /// An object in a compound file that is analogous to a file system directory. diff --git a/OpenMcdf3/StreamExtensions.cs b/OpenMcdf/StreamExtensions.cs similarity index 98% rename from OpenMcdf3/StreamExtensions.cs rename to OpenMcdf/StreamExtensions.cs index 5d904c00..5c6e2cb6 100644 --- a/OpenMcdf3/StreamExtensions.cs +++ b/OpenMcdf/StreamExtensions.cs @@ -2,7 +2,7 @@ using System.Buffers; #endif -namespace OpenMcdf3; +namespace OpenMcdf; internal static class StreamExtensions { diff --git a/OpenMcdf3/System/.editorconfig b/OpenMcdf/System/.editorconfig similarity index 100% rename from OpenMcdf3/System/.editorconfig rename to OpenMcdf/System/.editorconfig diff --git a/OpenMcdf3/System/.globalconfig b/OpenMcdf/System/.globalconfig similarity index 100% rename from OpenMcdf3/System/.globalconfig rename to OpenMcdf/System/.globalconfig diff --git a/OpenMcdf3/System/CompilerServices.cs b/OpenMcdf/System/CompilerServices.cs similarity index 100% rename from OpenMcdf3/System/CompilerServices.cs rename to OpenMcdf/System/CompilerServices.cs diff --git a/OpenMcdf3/System/Index.cs b/OpenMcdf/System/Index.cs similarity index 100% rename from OpenMcdf3/System/Index.cs rename to OpenMcdf/System/Index.cs diff --git a/OpenMcdf3/System/NullableAttributes.cs b/OpenMcdf/System/NullableAttributes.cs similarity index 100% rename from OpenMcdf3/System/NullableAttributes.cs rename to OpenMcdf/System/NullableAttributes.cs diff --git a/OpenMcdf3/System/Range.cs b/OpenMcdf/System/Range.cs similarity index 100% rename from OpenMcdf3/System/Range.cs rename to OpenMcdf/System/Range.cs diff --git a/OpenMcdf3/ThrowHelper.cs b/OpenMcdf/ThrowHelper.cs similarity index 99% rename from OpenMcdf3/ThrowHelper.cs rename to OpenMcdf/ThrowHelper.cs index 83ada20e..50c74084 100644 --- a/OpenMcdf3/ThrowHelper.cs +++ b/OpenMcdf/ThrowHelper.cs @@ -1,6 +1,6 @@ using System.Text; -namespace OpenMcdf3; +namespace OpenMcdf; /// /// Extensions to consistently throw exceptions. diff --git a/OpenMcdf3/TransactedStream.cs b/OpenMcdf/TransactedStream.cs similarity index 99% rename from OpenMcdf3/TransactedStream.cs rename to OpenMcdf/TransactedStream.cs index 42273a2a..dd616bee 100644 --- a/OpenMcdf3/TransactedStream.cs +++ b/OpenMcdf/TransactedStream.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace OpenMcdf3; +namespace OpenMcdf; internal class TransactedStream : Stream { diff --git a/StructuredStorageExplorer/MainForm.cs b/StructuredStorageExplorer/MainForm.cs index 4e64cb00..3ba8ec50 100644 --- a/StructuredStorageExplorer/MainForm.cs +++ b/StructuredStorageExplorer/MainForm.cs @@ -1,7 +1,7 @@ #define OLE_PROPERTY using OpenMcdf.Ole; -using OpenMcdf3; +using OpenMcdf; using StructuredStorageExplorer.Properties; using System.Collections; using System.ComponentModel; diff --git a/StructuredStorageExplorer/StreamDataProvider.cs b/StructuredStorageExplorer/StreamDataProvider.cs index 80aab085..e99c23b7 100644 --- a/StructuredStorageExplorer/StreamDataProvider.cs +++ b/StructuredStorageExplorer/StreamDataProvider.cs @@ -1,5 +1,5 @@ using Be.Windows.Forms; -using OpenMcdf3; +using OpenMcdf; namespace StructuredStorageExplorer; diff --git a/StructuredStorageExplorer/StructuredStorageExplorer.csproj b/StructuredStorageExplorer/StructuredStorageExplorer.csproj index d5002c46..0d089f2d 100644 --- a/StructuredStorageExplorer/StructuredStorageExplorer.csproj +++ b/StructuredStorageExplorer/StructuredStorageExplorer.csproj @@ -13,6 +13,6 @@ - + \ No newline at end of file From 36e34c77b6fee6e7bd2a81e17263ac358466c649 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 16:33:30 +1300 Subject: [PATCH 104/114] Allow switching streams --- OpenMcdf.Tests/RootStorageTests.cs | 37 +++++++++ OpenMcdf/CfbStream.cs | 14 ++-- OpenMcdf/ContextBase.cs | 16 ++++ OpenMcdf/DifatSectorEnumerator.cs | 29 ++++--- OpenMcdf/DirectoryEntries.cs | 28 +++---- OpenMcdf/Fat.cs | 41 +++++----- OpenMcdf/FatChainEnumerator.cs | 30 +++---- OpenMcdf/FatEnumerator.cs | 8 +- OpenMcdf/FatSectorEnumerator.cs | 35 ++++---- OpenMcdf/FatStream.cs | 48 +++++------ OpenMcdf/MiniFat.cs | 37 +++++---- OpenMcdf/MiniFatChainEnumerator.cs | 25 +++--- OpenMcdf/MiniFatEnumerator.cs | 15 ++-- OpenMcdf/MiniFatStream.cs | 42 +++++----- OpenMcdf/{IOContext.cs => RootContext.cs} | 17 ++-- OpenMcdf/RootContextSite.cs | 16 ++++ OpenMcdf/RootStorage.cs | 98 ++++++++++++++--------- OpenMcdf/Storage.cs | 43 +++++----- OpenMcdf/TransactedStream.cs | 4 +- 19 files changed, 338 insertions(+), 245 deletions(-) create mode 100644 OpenMcdf.Tests/RootStorageTests.cs create mode 100644 OpenMcdf/ContextBase.cs rename OpenMcdf/{IOContext.cs => RootContext.cs} (90%) create mode 100644 OpenMcdf/RootContextSite.cs diff --git a/OpenMcdf.Tests/RootStorageTests.cs b/OpenMcdf.Tests/RootStorageTests.cs new file mode 100644 index 00000000..9a328cd0 --- /dev/null +++ b/OpenMcdf.Tests/RootStorageTests.cs @@ -0,0 +1,37 @@ +namespace OpenMcdf.Tests; + +[TestClass] +public sealed class RootStorageTests +{ + [TestMethod] + [DataRow(Version.V3, 0)] + [DataRow(Version.V3, 1)] + [DataRow(Version.V3, 2)] + [DataRow(Version.V3, 4)] // Required 2 sectors including root + [DataRow(Version.V4, 0)] + [DataRow(Version.V4, 1)] + [DataRow(Version.V4, 2)] + [DataRow(Version.V4, 32)] // Required 2 sectors including root + public void SwitchStream(Version version, int subStorageCount) + { + using MemoryStream memoryStream = new(); + using MemoryStream switchedMemoryStream = new(); + using (var rootStorage = RootStorage.Create(memoryStream, version, StorageModeFlags.LeaveOpen)) + { + for (int i = 0; i < subStorageCount; i++) + rootStorage.CreateStorage($"Test{i}"); + + rootStorage.SwitchTo(switchedMemoryStream); + } + + memoryStream.Position = 0; + using (var rootStorage = RootStorage.Open(switchedMemoryStream, StorageModeFlags.LeaveOpen)) + { + IEnumerable entries = rootStorage.EnumerateEntries(); + Assert.AreEqual(subStorageCount, entries.Count()); + + for (int i = 0; i < subStorageCount; i++) + rootStorage.OpenStorage($"Test{i}"); + } + } +} diff --git a/OpenMcdf/CfbStream.cs b/OpenMcdf/CfbStream.cs index a905f177..e4159fc8 100644 --- a/OpenMcdf/CfbStream.cs +++ b/OpenMcdf/CfbStream.cs @@ -5,17 +5,17 @@ /// public sealed class CfbStream : Stream { - private readonly IOContext ioContext; + private readonly RootContextSite rootContextSite; private readonly DirectoryEntry directoryEntry; private Stream stream; - internal CfbStream(IOContext ioContext, DirectoryEntry directoryEntry) + internal CfbStream(RootContextSite rootContextSite, DirectoryEntry directoryEntry) { - this.ioContext = ioContext; + this.rootContextSite = rootContextSite; this.directoryEntry = directoryEntry; stream = directoryEntry.StreamLength < Header.MiniStreamCutoffSize - ? new MiniFatStream(ioContext, directoryEntry) - : new FatStream(ioContext, directoryEntry); + ? new MiniFatStream(rootContextSite, directoryEntry) + : new FatStream(rootContextSite, directoryEntry); } protected override void Dispose(bool disposing) @@ -53,7 +53,7 @@ public override void SetLength(long value) miniStream.Position = 0; DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); - FatStream fatStream = new(ioContext, newDirectoryEntry); + FatStream fatStream = new(rootContextSite, newDirectoryEntry); fatStream.SetLength(value); // Reserve enough space up front miniStream.CopyTo(fatStream); fatStream.Position = position; @@ -68,7 +68,7 @@ public override void SetLength(long value) fatStream.Position = 0; DirectoryEntry newDirectoryEntry = directoryEntry.Clone(); - MiniFatStream miniFatStream = new(ioContext, newDirectoryEntry); + MiniFatStream miniFatStream = new(rootContextSite, newDirectoryEntry); fatStream.SetLength(value); // Truncate the stream fatStream.CopyTo(miniFatStream); miniFatStream.Position = position; diff --git a/OpenMcdf/ContextBase.cs b/OpenMcdf/ContextBase.cs new file mode 100644 index 00000000..ccf84c5d --- /dev/null +++ b/OpenMcdf/ContextBase.cs @@ -0,0 +1,16 @@ +namespace OpenMcdf; + +/// +/// Supports switching the object. +/// +public abstract class ContextBase +{ + internal RootContextSite ContextSite { get; } + + internal RootContext Context => ContextSite.Context; + + internal ContextBase(RootContextSite site) + { + ContextSite = site; + } +} diff --git a/OpenMcdf/DifatSectorEnumerator.cs b/OpenMcdf/DifatSectorEnumerator.cs index a2c3589e..c48baf4d 100644 --- a/OpenMcdf/DifatSectorEnumerator.cs +++ b/OpenMcdf/DifatSectorEnumerator.cs @@ -3,19 +3,18 @@ namespace OpenMcdf; -internal class DifatSectorEnumerator : IEnumerator +internal class DifatSectorEnumerator : ContextBase, IEnumerator { - private readonly IOContext ioContext; public readonly uint DifatElementsPerSector; bool start = true; uint index = uint.MaxValue; Sector current = Sector.EndOfChain; private uint difatSectorId = SectorType.EndOfChain; - public DifatSectorEnumerator(IOContext ioContext) + public DifatSectorEnumerator(RootContextSite rootContextSite) + : base(rootContextSite) { - this.ioContext = ioContext; - DifatElementsPerSector = (uint)((ioContext.SectorSize / sizeof(uint)) - 1); + DifatElementsPerSector = (uint)((Context.SectorSize / sizeof(uint)) - 1); } /// @@ -45,7 +44,7 @@ public bool MoveNext() { start = false; index = uint.MaxValue; - difatSectorId = ioContext.Header.FirstDifatSectorId; + difatSectorId = Context.Header.FirstDifatSectorId; } uint nextIndex = index + 1; @@ -57,16 +56,16 @@ public bool MoveNext() return false; } - current = new(difatSectorId, ioContext.SectorSize); + current = new(difatSectorId, Context.SectorSize); index = nextIndex; - ioContext.Reader.Position = current.EndPosition - sizeof(uint); - difatSectorId = ioContext.Reader.ReadUInt32(); + Context.Reader.Position = current.EndPosition - sizeof(uint); + difatSectorId = Context.Reader.ReadUInt32(); return true; } public bool MoveTo(uint index) { - if (index >= ioContext.Header.DifatSectorCount) + if (index >= Context.Header.DifatSectorCount) return false; if (start && !MoveNext()) @@ -95,10 +94,10 @@ public void Reset() public void Add() { - Sector newDifatSector = new(ioContext.SectorCount, ioContext.SectorSize); + Sector newDifatSector = new(Context.SectorCount, Context.SectorSize); - Header header = ioContext.Header; - CfbBinaryWriter writer = ioContext.Writer; + Header header = Context.Header; + CfbBinaryWriter writer = Context.Writer; if (header.FirstDifatSectorId == SectorType.EndOfChain) { header.FirstDifatSectorId = newDifatSector.Id; @@ -118,10 +117,10 @@ public void Add() writer.Position = newDifatSector.EndPosition - sizeof(uint); writer.Write(SectorType.EndOfChain); - ioContext.ExtendStreamLength(newDifatSector.EndPosition); + Context.ExtendStreamLength(newDifatSector.EndPosition); header.DifatSectorCount++; - ioContext.Fat[newDifatSector.Id] = SectorType.Difat; + Context.Fat[newDifatSector.Id] = SectorType.Difat; start = false; index = header.DifatSectorCount - 1; diff --git a/OpenMcdf/DirectoryEntries.cs b/OpenMcdf/DirectoryEntries.cs index a6163fec..69ef5927 100644 --- a/OpenMcdf/DirectoryEntries.cs +++ b/OpenMcdf/DirectoryEntries.cs @@ -1,18 +1,17 @@ namespace OpenMcdf; -internal sealed class DirectoryEntries : IDisposable +internal sealed class DirectoryEntries : ContextBase, IDisposable { - private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; private readonly DirectoryEntryEnumerator directoryEntryEnumerator; private readonly int entriesPerSector; - public DirectoryEntries(IOContext ioContext) + public DirectoryEntries(RootContextSite rootContextSite) + : base(rootContextSite) { - this.ioContext = ioContext; - fatChainEnumerator = new FatChainEnumerator(ioContext, ioContext.Header.FirstDirectorySectorId); + fatChainEnumerator = new FatChainEnumerator(Context.Fat, Context.Header.FirstDirectorySectorId); directoryEntryEnumerator = new DirectoryEntryEnumerator(this); - entriesPerSector = ioContext.SectorSize / DirectoryEntry.Length; + entriesPerSector = Context.SectorSize / DirectoryEntry.Length; } public void Dispose() @@ -48,8 +47,8 @@ public bool TryGetDictionaryEntry(uint streamId, out DirectoryEntry? entry) return false; } - ioContext.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); - entry = ioContext.Reader.ReadDirectoryEntry(ioContext.Version, streamId); + Context.Reader.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); + entry = Context.Reader.ReadDirectoryEntry(Context.Version, streamId); return true; } @@ -59,15 +58,15 @@ public DirectoryEntry CreateOrRecycleDirectoryEntry() if (entry is not null) return entry; - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; uint id = fatChainEnumerator.Extend(); - Header header = ioContext.Header; + Header header = Context.Header; if (header.FirstDirectorySectorId == SectorType.EndOfChain) header.FirstDirectorySectorId = id; - if (ioContext.Version == Version.V4) + if (Context.Version == Version.V4) header.DirectorySectorCount++; - Sector sector = new(id, ioContext.SectorSize); + Sector sector = new(id, Context.SectorSize); writer.Position = sector.Position; for (int i = 0; i < entriesPerSector; i++) writer.Write(DirectoryEntry.Unallocated); @@ -97,8 +96,9 @@ public void Write(DirectoryEntry entry) if (!fatChainEnumerator.MoveTo(chainIndex)) throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); - CfbBinaryWriter writer = ioContext.Writer; - writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); + CfbBinaryWriter writer = Context.Writer; + Sector sector = new(fatChainEnumerator.Current.Value, Context.SectorSize); + writer.Position = sector.Position + (entryIndex * DirectoryEntry.Length); writer.Write(entry); } } diff --git a/OpenMcdf/Fat.cs b/OpenMcdf/Fat.cs index 30d75e39..487bd74b 100644 --- a/OpenMcdf/Fat.cs +++ b/OpenMcdf/Fat.cs @@ -7,21 +7,20 @@ namespace OpenMcdf; /// /// Encapsulates getting and setting entries in the FAT. /// -internal sealed class Fat : IEnumerable, IDisposable +internal sealed class Fat : ContextBase, IEnumerable, IDisposable { - private readonly IOContext ioContext; private readonly FatSectorEnumerator fatSectorEnumerator; internal readonly int FatElementsPerSector; private readonly byte[] cachedSectorBuffer; Sector cachedSector = Sector.EndOfChain; private bool isDirty; - public Fat(IOContext ioContext) + public Fat(RootContextSite rootContextSite) + : base(rootContextSite) { - this.ioContext = ioContext; - FatElementsPerSector = ioContext.SectorSize / sizeof(uint); - fatSectorEnumerator = new(ioContext); - cachedSectorBuffer = new byte[ioContext.SectorSize]; + FatElementsPerSector = Context.SectorSize / sizeof(uint); + fatSectorEnumerator = new(rootContextSite); + cachedSectorBuffer = new byte[Context.SectorSize]; } public void Dispose() @@ -61,7 +60,7 @@ void CacheCurrentSector() Flush(); - CfbBinaryReader reader = ioContext.Reader; + CfbBinaryReader reader = Context.Reader; reader.Position = current.Position; reader.Read(cachedSectorBuffer); cachedSector = current; @@ -71,7 +70,7 @@ public void Flush() { if (isDirty) { - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; writer.Position = cachedSector.Position; writer.Write(cachedSectorBuffer); isDirty = false; @@ -145,21 +144,21 @@ public uint Add(FatEnumerator fatEnumerator, uint startIndex) } FatEntry entry = fatEnumerator.Current; - Sector sector = new(entry.Index, ioContext.SectorSize); - ioContext.ExtendStreamLength(sector.EndPosition); + Sector sector = new(entry.Index, Context.SectorSize); + Context.ExtendStreamLength(sector.EndPosition); this[entry.Index] = SectorType.EndOfChain; return entry.Index; } - public IEnumerator GetEnumerator() => new FatEnumerator(ioContext); + public IEnumerator GetEnumerator() => new FatEnumerator(Context.Fat); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); internal void WriteTrace(TextWriter writer) { - byte[] data = new byte[ioContext.SectorSize]; + byte[] data = new byte[Context.SectorSize]; - Stream baseStream = ioContext.Reader.BaseStream; + Stream baseStream = Context.Reader.BaseStream; writer.WriteLine("Start of FAT ================="); @@ -168,7 +167,7 @@ internal void WriteTrace(TextWriter writer) foreach (FatEntry entry in this) { - Sector sector = new(entry.Index, ioContext.SectorSize); + Sector sector = new(entry.Index, Context.SectorSize); if (entry.IsFree) { freeCount++; @@ -196,8 +195,8 @@ internal void Validate() long difatSectorCount = 0; foreach (FatEntry entry in this) { - Sector sector = new(entry.Index, ioContext.SectorSize); - if (entry.Value <= SectorType.Maximum && sector.EndPosition > ioContext.Length) + Sector sector = new(entry.Index, Context.SectorSize); + if (entry.Value <= SectorType.Maximum && sector.EndPosition > Context.Length) throw new FormatException($"FAT entry {entry} is beyond the end of the stream."); if (entry.Value == SectorType.Fat) fatSectorCount++; @@ -205,10 +204,10 @@ internal void Validate() difatSectorCount++; } - if (ioContext.Header.FatSectorCount != fatSectorCount) - throw new FormatException($"FAT sector count mismatch. Expected: {ioContext.Header.FatSectorCount} Actual: {fatSectorCount}."); - if (ioContext.Header.DifatSectorCount != difatSectorCount) - throw new FormatException($"DIFAT sector count mismatch: Expected: {ioContext.Header.DifatSectorCount} Actual: {difatSectorCount}."); + if (Context.Header.FatSectorCount != fatSectorCount) + throw new FormatException($"FAT sector count mismatch. Expected: {Context.Header.FatSectorCount} Actual: {fatSectorCount}."); + if (Context.Header.DifatSectorCount != difatSectorCount) + throw new FormatException($"DIFAT sector count mismatch: Expected: {Context.Header.DifatSectorCount} Actual: {difatSectorCount}."); } internal long GetFreeSectorCount() => this.Count(entry => entry.IsFree); diff --git a/OpenMcdf/FatChainEnumerator.cs b/OpenMcdf/FatChainEnumerator.cs index 300fa0b3..a0725859 100644 --- a/OpenMcdf/FatChainEnumerator.cs +++ b/OpenMcdf/FatChainEnumerator.cs @@ -8,7 +8,7 @@ namespace OpenMcdf; /// internal sealed class FatChainEnumerator : IEnumerator { - private readonly IOContext ioContext; + private readonly Fat fat; private readonly FatEnumerator fatEnumerator; private uint startId; private bool start = true; @@ -16,11 +16,11 @@ internal sealed class FatChainEnumerator : IEnumerator private FatChainEntry current = FatChainEntry.Invalid; private long length = -1; - public FatChainEnumerator(IOContext ioContext, uint startSectorId) + public FatChainEnumerator(Fat fat, uint startSectorId) { - this.ioContext = ioContext; + this.fat = fat; startId = startSectorId; - fatEnumerator = new(ioContext); + fatEnumerator = new(fat); } /// @@ -29,7 +29,7 @@ public void Dispose() fatEnumerator.Dispose(); } - public Sector CurrentSector => new(Current.Value, ioContext.SectorSize); + public Sector CurrentSector => new(Current.Value, fat.Context.SectorSize); /// public FatChainEntry Current @@ -72,7 +72,7 @@ public bool MoveNext() return false; } - uint value = ioContext.Fat[current.Value]; + uint value = fat[current.Value]; if (value is SectorType.EndOfChain) { index = uint.MaxValue; @@ -147,7 +147,7 @@ public uint Extend(uint requiredChainLength) if (startId == StreamId.NoStream) { - startId = ioContext.Fat.Add(fatEnumerator, 0); + startId = fat.Add(fatEnumerator, 0); chainLength = 1; } @@ -159,8 +159,8 @@ public uint Extend(uint requiredChainLength) Debug.Assert(ok); while (chainLength < requiredChainLength) { - uint id = ioContext.Fat.Add(fatEnumerator, lastId); - ioContext.Fat[lastId] = id; + uint id = fat.Add(fatEnumerator, lastId); + fat[lastId] = id; lastId = id; chainLength++; } @@ -173,7 +173,7 @@ public uint ExtendFrom(uint hintId) { if (startId == SectorType.EndOfChain) { - startId = ioContext.Fat.Add(fatEnumerator, hintId); + startId = fat.Add(fatEnumerator, hintId); return startId; } @@ -183,8 +183,8 @@ public uint ExtendFrom(uint hintId) lastId = current.Value; } - uint id = ioContext.Fat.Add(fatEnumerator, lastId); - ioContext.Fat[lastId] = id; + uint id = fat.Add(fatEnumerator, lastId); + fat[lastId] = id; return id; } @@ -202,15 +202,15 @@ public uint Shrink(uint requiredChainLength) if (lastId is not SectorType.EndOfChain and not SectorType.Free) { if (index == requiredChainLength) - ioContext.Fat[lastId] = SectorType.EndOfChain; + fat[lastId] = SectorType.EndOfChain; else if (index > requiredChainLength) - ioContext.Fat[lastId] = SectorType.Free; + fat[lastId] = SectorType.Free; } lastId = current.Value; } - ioContext.Fat[lastId] = SectorType.Free; + fat[lastId] = SectorType.Free; if (requiredChainLength == 0) { diff --git a/OpenMcdf/FatEnumerator.cs b/OpenMcdf/FatEnumerator.cs index 4d8eed07..afeb1cd1 100644 --- a/OpenMcdf/FatEnumerator.cs +++ b/OpenMcdf/FatEnumerator.cs @@ -7,14 +7,14 @@ namespace OpenMcdf; /// internal class FatEnumerator : IEnumerator { - readonly IOContext ioContext; + readonly Fat fat; bool start = true; uint index = uint.MaxValue; uint value = uint.MaxValue; - public FatEnumerator(IOContext ioContext) + public FatEnumerator(Fat fat) { - this.ioContext = ioContext; + this.fat = fat; } /// @@ -60,7 +60,7 @@ public bool MoveTo(uint index) if (this.index == index) return true; - if (ioContext.Fat.TryGetValue(index, out value)) + if (fat.TryGetValue(index, out value)) { this.index = index; return true; diff --git a/OpenMcdf/FatSectorEnumerator.cs b/OpenMcdf/FatSectorEnumerator.cs index 1381dc9e..9ab32d26 100644 --- a/OpenMcdf/FatSectorEnumerator.cs +++ b/OpenMcdf/FatSectorEnumerator.cs @@ -6,24 +6,23 @@ namespace OpenMcdf; /// /// Enumerates the FAT sectors of a compound file. /// -internal sealed class FatSectorEnumerator : IEnumerator +internal sealed class FatSectorEnumerator : ContextBase, IEnumerator { - private readonly IOContext ioContext; private readonly DifatSectorEnumerator difatSectorEnumerator; private bool start = true; private uint index = uint.MaxValue; private Sector current = Sector.EndOfChain; - public FatSectorEnumerator(IOContext ioContext) + public FatSectorEnumerator(RootContextSite rootContextSite) + : base(rootContextSite) { - this.ioContext = ioContext; - difatSectorEnumerator = new(ioContext); + difatSectorEnumerator = new(rootContextSite); } /// public void Dispose() { - // IOContext is owned by a parent + // Context is owned by a parent difatSectorEnumerator.Dispose(); } @@ -68,7 +67,7 @@ public bool MoveTo(uint index) if (index == this.index) return true; - if (index >= ioContext.Header.FatSectorCount) + if (index >= Context.Header.FatSectorCount) { this.index = uint.MaxValue; current = Sector.EndOfChain; @@ -78,8 +77,8 @@ public bool MoveTo(uint index) if (index < Header.DifatArrayLength) { this.index = index; - uint difatId = ioContext.Header.Difat[this.index]; - current = new(difatId, ioContext.SectorSize); + uint difatId = Context.Header.Difat[this.index]; + current = new(difatId, Context.SectorSize); return true; } @@ -97,10 +96,10 @@ public bool MoveTo(uint index) } Sector difatSector = difatSectorEnumerator.Current; - ioContext.Reader.Position = difatSector.Position + (difatElementIndex * sizeof(uint)); - uint id = ioContext.Reader.ReadUInt32(); + Context.Reader.Position = difatSector.Position + (difatElementIndex * sizeof(uint)); + uint id = Context.Reader.ReadUInt32(); this.index = index; - current = new Sector(id, ioContext.SectorSize); + current = new Sector(id, Context.SectorSize); return true; } @@ -135,14 +134,14 @@ public void Reset() public uint Add() { // No FAT sectors are free, so add a new one - Header header = ioContext.Header; - uint nextIndex = ioContext.Header.FatSectorCount; - Sector newFatSector = new(ioContext.SectorCount, ioContext.SectorSize); + Header header = Context.Header; + uint nextIndex = Context.Header.FatSectorCount; + Sector newFatSector = new(Context.SectorCount, Context.SectorSize); - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; writer.Position = newFatSector.Position; writer.Write(SectorDataCache.GetFatEntryData(newFatSector.Length)); - ioContext.ExtendStreamLength(newFatSector.EndPosition); + Context.ExtendStreamLength(newFatSector.EndPosition); header.FatSectorCount++; @@ -164,7 +163,7 @@ public uint Add() writer.Write(newFatSector.Id); } - ioContext.Fat[newFatSector.Id] = SectorType.Fat; + Context.Fat[newFatSector.Id] = SectorType.Fat; return newFatSector.Id; } } diff --git a/OpenMcdf/FatStream.cs b/OpenMcdf/FatStream.cs index d3ec8f59..bede4309 100644 --- a/OpenMcdf/FatStream.cs +++ b/OpenMcdf/FatStream.cs @@ -5,23 +5,25 @@ /// internal class FatStream : Stream { - readonly IOContext ioContext; + readonly RootContextSite rootContextSite; readonly FatChainEnumerator chain; long position; bool isDirty; bool disposed; - internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) + private RootContext Context => rootContextSite.Context; + + internal FatStream(RootContextSite rootContextSite, DirectoryEntry directoryEntry) { - this.ioContext = ioContext; + this.rootContextSite = rootContextSite; DirectoryEntry = directoryEntry; - chain = new(ioContext, directoryEntry.StartSectorId); + chain = new(Context.Fat, directoryEntry.StartSectorId); } /// internal DirectoryEntry DirectoryEntry { get; private set; } - internal long ChainCapacity => ((Length + ioContext.SectorSize - 1) / ioContext.SectorSize) * ioContext.SectorSize; + internal long ChainCapacity => ((Length + Context.SectorSize - 1) / Context.SectorSize) * Context.SectorSize; /// public override bool CanRead => true; @@ -30,7 +32,7 @@ internal FatStream(IOContext ioContext, DirectoryEntry directoryEntry) public override bool CanSeek => true; /// - public override bool CanWrite => ioContext.CanWrite; + public override bool CanWrite => Context.CanWrite; /// public override long Length => DirectoryEntry.StreamLength; @@ -63,12 +65,12 @@ public override void Flush() if (isDirty) { - ioContext.DirectoryEntries.Write(DirectoryEntry); + Context.DirectoryEntries.Write(DirectoryEntry); isDirty = false; } if (CanWrite) - ioContext.Writer!.Flush(); + Context.Writer!.Flush(); } /// @@ -85,7 +87,7 @@ public override int Read(byte[] buffer, int offset, int count) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; @@ -93,12 +95,12 @@ public override int Read(byte[] buffer, int offset, int count) int readCount = 0; do { - Sector sector = chain.CurrentSector; + Sector sector = new(chain.Current.Value, Context.SectorSize); int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); - ioContext.Reader.Position = sector.Position + sectorOffset; + Context.Reader.Position = sector.Position + sectorOffset; int localOffset = offset + readCount; - int read = ioContext.Reader.Read(buffer, localOffset, (int)readLength); + int read = Context.Reader.Read(buffer, localOffset, (int)readLength); if (read == 0) return readCount; position += read; @@ -148,10 +150,10 @@ public override void SetLength(long value) { this.ThrowIfNotWritable(); - uint requiredChainLength = (uint)((value + ioContext.SectorSize - 1) / ioContext.SectorSize); + uint requiredChainLength = (uint)((value + Context.SectorSize - 1) / Context.SectorSize); if (value > ChainCapacity) DirectoryEntry.StartSectorId = chain.Extend(requiredChainLength); - else if (value <= ChainCapacity - ioContext.SectorSize) + else if (value <= ChainCapacity - Context.SectorSize) DirectoryEntry.StartSectorId = chain.Shrink(requiredChainLength); DirectoryEntry.StreamLength = value; @@ -169,9 +171,9 @@ public override void Write(byte[] buffer, int offset, int count) if (count == 0) return; - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; int writeCount = 0; uint lastIndex = 0; for (; ; ) @@ -185,7 +187,7 @@ public override void Write(byte[] buffer, int offset, int count) int localOffset = offset + writeCount; long writeLength = Math.Min(remaining, sector.Length - sectorOffset); writer.Write(buffer, localOffset, (int)writeLength); - ioContext.ExtendStreamLength(sector.EndPosition); + Context.ExtendStreamLength(sector.EndPosition); position += writeLength; writeCount += (int)writeLength; if (position > Length) @@ -218,7 +220,7 @@ public override int Read(Span buffer) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; @@ -229,10 +231,10 @@ public override int Read(Span buffer) Sector sector = chain.CurrentSector; int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); - ioContext.Reader.Position = sector.Position + sectorOffset; + Context.Reader.Position = sector.Position + sectorOffset; int localOffset = readCount; Span slice = buffer.Slice(localOffset, (int)readLength); - int read = ioContext.Reader.Read(slice); + int read = Context.Reader.Read(slice); if (read == 0) return readCount; position += read; @@ -255,9 +257,9 @@ public override void Write(ReadOnlySpan buffer) if (buffer.Length == 0) return; - uint chainIndex = (uint)Math.DivRem(position, ioContext.SectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; int writeCount = 0; uint lastIndex = 0; for (; ; ) @@ -272,7 +274,7 @@ public override void Write(ReadOnlySpan buffer) long writeLength = Math.Min(remaining, sector.Length - sectorOffset); ReadOnlySpan slice = buffer.Slice(localOffset, (int)writeLength); writer.Write(slice); - ioContext.ExtendStreamLength(sector.EndPosition); + Context.ExtendStreamLength(sector.EndPosition); position += writeLength; writeCount += (int)writeLength; if (position > Length) diff --git a/OpenMcdf/MiniFat.cs b/OpenMcdf/MiniFat.cs index 1ed7bc71..8faa235c 100644 --- a/OpenMcdf/MiniFat.cs +++ b/OpenMcdf/MiniFat.cs @@ -7,20 +7,19 @@ namespace OpenMcdf; /// /// Encapsulates getting and setting entries in the mini FAT. /// -internal sealed class MiniFat : IEnumerable, IDisposable +internal sealed class MiniFat : ContextBase, IEnumerable, IDisposable { - private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; private readonly int ElementsPerSector; private readonly byte[] sector; private bool isDirty; - public MiniFat(IOContext ioContext) + public MiniFat(RootContextSite rootContextSite) + : base(rootContextSite) { - this.ioContext = ioContext; - ElementsPerSector = ioContext.SectorSize / sizeof(uint); - fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); - sector = new byte[ioContext.SectorSize]; + ElementsPerSector = Context.SectorSize / sizeof(uint); + fatChainEnumerator = new(Context.Fat, Context.Header.FirstMiniFatSectorId); + sector = new byte[Context.SectorSize]; } public void Dispose() @@ -34,14 +33,14 @@ public void Flush() { if (isDirty) { - CfbBinaryWriter writer = ioContext.Writer; + CfbBinaryWriter writer = Context.Writer; writer.Position = fatChainEnumerator.CurrentSector.Position; writer.Write(sector); isDirty = false; } } - public IEnumerator GetEnumerator() => new MiniFatEnumerator(ioContext); + public IEnumerator GetEnumerator() => new MiniFatEnumerator(ContextSite); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -72,7 +71,7 @@ bool TryMoveToSectorForKey(uint key, out long elementIndex) if (!ok) return false; - CfbBinaryReader reader = ioContext.Reader; + CfbBinaryReader reader = Context.Reader; reader.Position = fatChainEnumerator.CurrentSector.Position; reader.Read(sector); return true; @@ -114,12 +113,12 @@ public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) if (!movedToFreeEntry) { uint newSectorIndex = fatChainEnumerator.Extend(); - Sector sector = new(newSectorIndex, ioContext.SectorSize); - CfbBinaryWriter writer = ioContext.Writer; + Sector sector = new(newSectorIndex, Context.SectorSize); + CfbBinaryWriter writer = Context.Writer; writer.Position = sector.Position; writer.Write(SectorDataCache.GetFatEntryData(sector.Length)); - Header header = ioContext.Header; + Header header = Context.Header; if (header.FirstMiniFatSectorId == SectorType.EndOfChain) header.FirstMiniFatSectorId = newSectorIndex; header.MiniFatSectorCount++; @@ -134,16 +133,16 @@ public uint Add(MiniFatEnumerator miniFatEnumerator, uint startIndex) this[entry.Index] = SectorType.EndOfChain; Debug.Assert(entry.IsFree); - MiniSector miniSector = new(entry.Index, ioContext.MiniSectorSize); - if (ioContext.MiniStream.Length < miniSector.EndPosition) - ioContext.MiniStream.SetLength(miniSector.EndPosition); + MiniSector miniSector = new(entry.Index, Context.MiniSectorSize); + if (Context.MiniStream.Length < miniSector.EndPosition) + Context.MiniStream.SetLength(miniSector.EndPosition); return entry.Index; } internal void Trace(TextWriter writer) { - using MiniFatEnumerator miniFatEnumerator = new(ioContext); + using MiniFatEnumerator miniFatEnumerator = new(ContextSite); writer.WriteLine("Start of Mini FAT ============"); while (miniFatEnumerator.MoveNext()) @@ -153,12 +152,12 @@ internal void Trace(TextWriter writer) internal void Validate() { - using MiniFatEnumerator miniFatEnumerator = new(ioContext); + using MiniFatEnumerator miniFatEnumerator = new(ContextSite); while (miniFatEnumerator.MoveNext()) { FatEntry current = miniFatEnumerator.Current; - if (current.Value <= SectorType.Maximum && miniFatEnumerator.CurrentSector.EndPosition > ioContext.MiniStream.Length) + if (current.Value <= SectorType.Maximum && miniFatEnumerator.CurrentSector.EndPosition > Context.MiniStream.Length) { throw new FormatException($"Mini FAT entry {current} is beyond the end of the mini stream."); } diff --git a/OpenMcdf/MiniFatChainEnumerator.cs b/OpenMcdf/MiniFatChainEnumerator.cs index 1b02e56b..d6ff4cfc 100644 --- a/OpenMcdf/MiniFatChainEnumerator.cs +++ b/OpenMcdf/MiniFatChainEnumerator.cs @@ -6,9 +6,8 @@ namespace OpenMcdf; /// /// Enumerates the s in a Mini FAT sector chain. /// -internal sealed class MiniFatChainEnumerator : IEnumerator +internal sealed class MiniFatChainEnumerator : ContextBase, IEnumerator { - private readonly IOContext ioContext; private readonly MiniFatEnumerator miniFatEnumerator; private uint startId; private bool start = true; @@ -16,11 +15,11 @@ internal sealed class MiniFatChainEnumerator : IEnumerator private FatChainEntry current = FatChainEntry.Invalid; private long length = -1; - public MiniFatChainEnumerator(IOContext ioContext, uint startSectorId) + public MiniFatChainEnumerator(RootContextSite rootContextSite, uint startSectorId) + : base(rootContextSite) { - this.ioContext = ioContext; startId = startSectorId; - miniFatEnumerator = new(ioContext); + miniFatEnumerator = new(rootContextSite); } /// @@ -36,7 +35,7 @@ public void Dispose() public uint Index => index; - public MiniSector CurrentSector => new(Current.Value, ioContext.MiniSectorSize); + public MiniSector CurrentSector => new(Current.Value, Context.MiniSectorSize); /// public FatChainEntry Current @@ -63,7 +62,7 @@ public bool MoveNext() } else if (!current.IsFreeOrEndOfChain) { - uint sectorId = ioContext.MiniFat[current.Value]; + uint sectorId = Context.MiniFat[current.Value]; if (sectorId == SectorType.EndOfChain) { index = uint.MaxValue; @@ -132,7 +131,7 @@ public void Extend(uint requiredChainLength) if (startId == StreamId.NoStream) { - startId = ioContext.MiniFat.Add(miniFatEnumerator, 0); + startId = Context.MiniFat.Add(miniFatEnumerator, 0); chainLength = 1; } @@ -144,8 +143,8 @@ public void Extend(uint requiredChainLength) Debug.Assert(ok); while (chainLength < requiredChainLength) { - uint id = ioContext.MiniFat.Add(miniFatEnumerator, lastId); - ioContext.MiniFat[lastId] = id; + uint id = Context.MiniFat.Add(miniFatEnumerator, lastId); + Context.MiniFat[lastId] = id; lastId = id; chainLength++; } @@ -173,16 +172,16 @@ public void Shrink(uint requiredChainLength) if (lastId <= SectorType.Maximum) { if (index == requiredChainLength) - ioContext.MiniFat[lastId] = SectorType.EndOfChain; + Context.MiniFat[lastId] = SectorType.EndOfChain; else if (index > requiredChainLength) - ioContext.MiniFat[lastId] = SectorType.Free; + Context.MiniFat[lastId] = SectorType.Free; } lastId = current.Value; } if (lastId <= SectorType.Maximum) - ioContext.MiniFat[lastId] = SectorType.Free; + Context.MiniFat[lastId] = SectorType.Free; if (requiredChainLength == 0) { diff --git a/OpenMcdf/MiniFatEnumerator.cs b/OpenMcdf/MiniFatEnumerator.cs index d7cd55a0..e5ad35f3 100644 --- a/OpenMcdf/MiniFatEnumerator.cs +++ b/OpenMcdf/MiniFatEnumerator.cs @@ -5,18 +5,17 @@ namespace OpenMcdf; /// /// Enumerates the s from the Mini FAT. /// -internal sealed class MiniFatEnumerator : IEnumerator +internal sealed class MiniFatEnumerator : ContextBase, IEnumerator { - private readonly IOContext ioContext; private readonly FatChainEnumerator fatChainEnumerator; private bool start = true; private uint index = uint.MaxValue; private uint value = uint.MaxValue; - public MiniFatEnumerator(IOContext ioContext) + public MiniFatEnumerator(RootContextSite rootContextSite) + : base(rootContextSite) { - fatChainEnumerator = new(ioContext, ioContext.Header.FirstMiniFatSectorId); - this.ioContext = ioContext; + fatChainEnumerator = new(Context.Fat, Context.Header.FirstMiniFatSectorId); } /// @@ -31,7 +30,7 @@ public MiniSector CurrentSector { if (index == uint.MaxValue) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); - return new(value, ioContext.MiniSectorSize); + return new(value, Context.MiniSectorSize); } } @@ -72,7 +71,7 @@ public bool MoveTo(uint index) if (this.index == index) return true; - if (ioContext.MiniFat.TryGetValue(index, out value)) + if (Context.MiniFat.TryGetValue(index, out value)) { this.index = index; return true; @@ -98,7 +97,7 @@ public bool MoveNextFreeEntry() /// public void Reset() { - fatChainEnumerator.Reset(ioContext.Header.FirstMiniFatSectorId); + fatChainEnumerator.Reset(Context.Header.FirstMiniFatSectorId); start = true; index = uint.MaxValue; value = uint.MaxValue; diff --git a/OpenMcdf/MiniFatStream.cs b/OpenMcdf/MiniFatStream.cs index 6afaf9f5..69699e02 100644 --- a/OpenMcdf/MiniFatStream.cs +++ b/OpenMcdf/MiniFatStream.cs @@ -5,28 +5,30 @@ /// internal sealed class MiniFatStream : Stream { - readonly IOContext ioContext; + readonly RootContextSite rootContextSite; readonly MiniFatChainEnumerator miniChain; long position; bool disposed; bool isDirty; - internal MiniFatStream(IOContext ioContext, DirectoryEntry directoryEntry) + internal MiniFatStream(RootContextSite rootContextSite, DirectoryEntry directoryEntry) { - this.ioContext = ioContext; + this.rootContextSite = rootContextSite; DirectoryEntry = directoryEntry; - miniChain = new(ioContext, directoryEntry.StartSectorId); + miniChain = new(rootContextSite, directoryEntry.StartSectorId); } + RootContext Context => rootContextSite.Context; + internal DirectoryEntry DirectoryEntry { get; private set; } - internal long ChainCapacity => ((Length + ioContext.MiniSectorSize - 1) / ioContext.MiniSectorSize) * ioContext.MiniSectorSize; + internal long ChainCapacity => ((Length + Context.MiniSectorSize - 1) / Context.MiniSectorSize) * Context.MiniSectorSize; public override bool CanRead => true; public override bool CanSeek => true; - public override bool CanWrite => ioContext.CanWrite; + public override bool CanWrite => Context.CanWrite; public override long Length => DirectoryEntry.StreamLength; @@ -55,11 +57,11 @@ public override void Flush() if (isDirty) { - ioContext.DirectoryEntries.Write(DirectoryEntry); + Context.DirectoryEntries.Write(DirectoryEntry); isDirty = false; } - ioContext.MiniStream.Flush(); + Context.MiniStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) @@ -75,11 +77,11 @@ public override int Read(byte[] buffer, int offset, int count) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) return 0; - FatStream miniStream = ioContext.MiniStream; + FatStream miniStream = Context.MiniStream; int realCount = Math.Min(count, maxCount); int readCount = 0; do @@ -141,10 +143,10 @@ public override void SetLength(long value) this.ThrowIfDisposed(disposed); this.ThrowIfNotWritable(); - uint requiredChainLength = (uint)((value + ioContext.MiniSectorSize - 1) / ioContext.MiniSectorSize); + uint requiredChainLength = (uint)((value + Context.MiniSectorSize - 1) / Context.MiniSectorSize); if (value > ChainCapacity) miniChain.Extend(requiredChainLength); - else if (value <= ChainCapacity - ioContext.MiniSectorSize) + else if (value <= ChainCapacity - Context.MiniSectorSize) miniChain.Shrink(requiredChainLength); DirectoryEntry.StartSectorId = miniChain.StartId; @@ -165,11 +167,11 @@ public override void Write(byte[] buffer, int offset, int count) if (position + count > ChainCapacity) SetLength(position + count); - uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); - FatStream miniStream = ioContext.MiniStream; + FatStream miniStream = Context.MiniStream; int writeCount = 0; do { @@ -178,7 +180,7 @@ public override void Write(byte[] buffer, int offset, int count) miniStream.Seek(basePosition, SeekOrigin.Begin); int remaining = count - writeCount; int localOffset = offset + writeCount; - long writeLength = Math.Min(remaining, ioContext.MiniSectorSize - sectorOffset); + long writeLength = Math.Min(remaining, Context.MiniSectorSize - sectorOffset); miniStream.Write(buffer, localOffset, (int)writeLength); position += writeLength; writeCount += (int)writeLength; @@ -210,11 +212,11 @@ public override int Read(Span buffer) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) return 0; - FatStream miniStream = ioContext.MiniStream; + FatStream miniStream = Context.MiniStream; int realCount = Math.Min(buffer.Length, maxCount); int readCount = 0; do @@ -251,11 +253,11 @@ public override void Write(ReadOnlySpan buffer) if (position + buffer.Length > ChainCapacity) SetLength(position + buffer.Length); - uint chainIndex = (uint)Math.DivRem(position, ioContext.MiniSectorSize, out long sectorOffset); + uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); - FatStream miniStream = ioContext.MiniStream; + FatStream miniStream = Context.MiniStream; int writeCount = 0; do { @@ -264,7 +266,7 @@ public override void Write(ReadOnlySpan buffer) miniStream.Seek(basePosition, SeekOrigin.Begin); int remaining = buffer.Length - writeCount; int localOffset = writeCount; - long writeLength = Math.Min(remaining, ioContext.MiniSectorSize - sectorOffset); + long writeLength = Math.Min(remaining, Context.MiniSectorSize - sectorOffset); ReadOnlySpan slice = buffer.Slice(localOffset, (int)writeLength); miniStream.Write(slice); position += writeLength; diff --git a/OpenMcdf/IOContext.cs b/OpenMcdf/RootContext.cs similarity index 90% rename from OpenMcdf/IOContext.cs rename to OpenMcdf/RootContext.cs index da0cefd4..df72b3d0 100644 --- a/OpenMcdf/IOContext.cs +++ b/OpenMcdf/RootContext.cs @@ -11,7 +11,7 @@ enum IOContextFlags /// /// Encapsulates the objects required to read and write data to and from a compound file. /// -internal sealed class IOContext : IDisposable +internal sealed class RootContext : ContextBase, IDisposable { readonly IOContextFlags contextFlags; readonly CfbBinaryWriter? writer; @@ -45,7 +45,7 @@ public MiniFat MiniFat { get { - miniFat ??= new(this); + miniFat ??= new(ContextSite); return miniFat; } } @@ -54,7 +54,7 @@ public FatStream MiniStream { get { - miniStream ??= new(this, RootEntry); + miniStream ??= new(ContextSite, RootEntry); return miniStream; } } @@ -78,8 +78,11 @@ public FatStream MiniStream bool isDirty; - public IOContext(Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) + public RootContext(RootContextSite rootContextSite, Stream stream, Version version, IOContextFlags contextFlags = IOContextFlags.None) + : base(rootContextSite) { + rootContextSite.Switch(this); + BaseStream = stream; this.contextFlags = contextFlags; @@ -101,8 +104,8 @@ public IOContext(Stream stream, Version version, IOContextFlags contextFlags = I if (stream.CanWrite) writer = new(actualStream); - Fat = new(this); - DirectoryEntries = new(this); + Fat = new(ContextSite); + DirectoryEntries = new(ContextSite); if (contextFlags.HasFlag(IOContextFlags.Create)) { @@ -142,6 +145,8 @@ public void Dispose() public void Flush() { + Fat.Flush(); + if (isDirty && writer is not null && transactedStream is null) { // Ensure the stream is as long as expected diff --git a/OpenMcdf/RootContextSite.cs b/OpenMcdf/RootContextSite.cs new file mode 100644 index 00000000..e4c60455 --- /dev/null +++ b/OpenMcdf/RootContextSite.cs @@ -0,0 +1,16 @@ +namespace OpenMcdf; + +/// +/// A site for the object, to allow switching streams. +/// +internal class RootContextSite +{ + RootContext? context; + + internal RootContext Context => context!; + + internal void Switch(RootContext context) + { + this.context = context; + } +} diff --git a/OpenMcdf/RootStorage.cs b/OpenMcdf/RootStorage.cs index 1120a8a4..2f652008 100644 --- a/OpenMcdf/RootStorage.cs +++ b/OpenMcdf/RootStorage.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf; +using System.Runtime.CompilerServices; + +namespace OpenMcdf; public enum Version : ushort { @@ -28,6 +30,16 @@ private static void ThrowIfLeaveOpen(StorageModeFlags flags) throw new ArgumentException($"{StorageModeFlags.LeaveOpen} is not valid for files"); } + private static IOContextFlags ToIOContextFlags(StorageModeFlags flags) + { + IOContextFlags contextFlags = IOContextFlags.None; + if (flags.HasFlag(StorageModeFlags.LeaveOpen)) + contextFlags |= IOContextFlags.LeaveOpen; + if (flags.HasFlag(StorageModeFlags.Transacted)) + contextFlags |= IOContextFlags.Transacted; + return contextFlags; + } + public static RootStorage Create(string fileName, Version version = Version.V3, StorageModeFlags flags = StorageModeFlags.None) { ThrowIfLeaveOpen(flags); @@ -42,14 +54,10 @@ public static RootStorage Create(Stream stream, Version version = Version.V3, St stream.SetLength(0); stream.Position = 0; - IOContextFlags contextFlags = IOContextFlags.Create; - if (flags.HasFlag(StorageModeFlags.LeaveOpen)) - contextFlags |= IOContextFlags.LeaveOpen; - if (flags.HasFlag(StorageModeFlags.Transacted)) - contextFlags |= IOContextFlags.Transacted; - - IOContext ioContext = new(stream, version, contextFlags); - return new RootStorage(ioContext, flags); + IOContextFlags contextFlags = ToIOContextFlags(flags) | IOContextFlags.Create; + RootContextSite rootContextSite = new(); + _ = new RootContext(rootContextSite, stream, version, contextFlags); + return new RootStorage(rootContextSite, flags); } public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags flags = StorageModeFlags.None) @@ -73,29 +81,25 @@ public static RootStorage Open(Stream stream, StorageModeFlags flags = StorageMo stream.ThrowIfNotSeekable(); stream.Position = 0; - IOContextFlags contextFlags = IOContextFlags.None; - if (flags.HasFlag(StorageModeFlags.LeaveOpen)) - contextFlags |= IOContextFlags.LeaveOpen; - if (flags.HasFlag(StorageModeFlags.Transacted)) - contextFlags |= IOContextFlags.Transacted; - - IOContext ioContext = new(stream, Version.Unknown, contextFlags); - return new RootStorage(ioContext, flags); + IOContextFlags contextFlags = ToIOContextFlags(flags); + RootContextSite rootContextSite = new(); + _ = new RootContext(rootContextSite, stream, Version.Unknown, contextFlags); + return new RootStorage(rootContextSite, flags); } - RootStorage(IOContext ioContext, StorageModeFlags storageModeFlags) - : base(ioContext, ioContext.RootEntry) + RootStorage(RootContextSite rootContextSite, StorageModeFlags storageModeFlags) + : base(rootContextSite, rootContextSite.Context.RootEntry) { this.storageModeFlags = storageModeFlags; } - public void Dispose() => ioContext?.Dispose(); + public void Dispose() => Context?.Dispose(); public void Flush(bool consolidate = false) { - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); - ioContext.Flush(); + Context.Flush(); if (consolidate) Consolidate(); @@ -109,21 +113,21 @@ void Consolidate() try { - if (ioContext.BaseStream is MemoryStream) - destinationStream = new MemoryStream((int)ioContext.BaseStream.Length); - else if (ioContext.BaseStream is FileStream) + if (Context.BaseStream is MemoryStream) + destinationStream = new MemoryStream((int)Context.BaseStream.Length); + else if (Context.BaseStream is FileStream) destinationStream = File.Create(Path.GetTempFileName()); else throw new NotSupportedException("Unsupported stream type for consolidation."); - using (RootStorage destinationStorage = Create(destinationStream, ioContext.Version, storageModeFlags)) + using (RootStorage destinationStorage = Create(destinationStream, Context.Version, storageModeFlags)) CopyTo(destinationStorage); - ioContext.BaseStream.Position = 0; + Context.BaseStream.Position = 0; destinationStream.Position = 0; - destinationStream.CopyTo(ioContext.BaseStream); - ioContext.BaseStream.SetLength(destinationStream.Length); + destinationStream.CopyTo(Context.BaseStream); + Context.BaseStream.SetLength(destinationStream.Length); } catch { @@ -138,28 +142,46 @@ void Consolidate() public void Commit() { - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); - ioContext.Commit(); + Context.Commit(); } public void Revert() { - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); + + Context.Revert(); + } + + public void SwitchTo(Stream stream) + { + Flush(); + + stream.SetLength(Context.BaseStream.Length); + + stream.Position = 0; + Context.BaseStream.Position = 0; + + Context.BaseStream.CopyTo(stream); + stream.Position = 0; + + Context.Dispose(); - ioContext.Revert(); + IOContextFlags contextFlags = ToIOContextFlags(storageModeFlags); + _ = new RootContext(ContextSite, stream, Version.Unknown, contextFlags); } internal void Trace(TextWriter writer) { - writer.WriteLine(ioContext.Header); - ioContext.Fat.WriteTrace(writer); - ioContext.MiniFat.Trace(writer); + writer.WriteLine(Context.Header); + Context.Fat.WriteTrace(writer); + Context.MiniFat.Trace(writer); } internal void Validate() { - ioContext.Fat.Validate(); - ioContext.MiniFat.Validate(); + Context.Fat.Validate(); + Context.MiniFat.Validate(); } } diff --git a/OpenMcdf/Storage.cs b/OpenMcdf/Storage.cs index 27b01609..8bc07bd7 100644 --- a/OpenMcdf/Storage.cs +++ b/OpenMcdf/Storage.cs @@ -3,20 +3,19 @@ /// /// An object in a compound file that is analogous to a file system directory. /// -public class Storage +public class Storage : ContextBase { - internal readonly IOContext ioContext; internal readonly DirectoryTree directoryTree; internal DirectoryEntry DirectoryEntry { get; } - internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) + internal Storage(RootContextSite rootContextSite, DirectoryEntry directoryEntry) + : base(rootContextSite) { if (directoryEntry.Type is not StorageType.Storage and not StorageType.Root) throw new ArgumentException("DirectoryEntry must be a Storage or Root.", nameof(directoryEntry)); - this.ioContext = ioContext; - directoryTree = new(ioContext.DirectoryEntries, directoryEntry); + directoryTree = new(Context.DirectoryEntries, directoryEntry); DirectoryEntry = directoryEntry; } @@ -24,7 +23,7 @@ internal Storage(IOContext ioContext, DirectoryEntry directoryEntry) public IEnumerable EnumerateEntries() { - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); return EnumerateDirectoryEntries() .Select(e => e.ToEntryInfo()); @@ -32,7 +31,7 @@ public IEnumerable EnumerateEntries() IEnumerable EnumerateDirectoryEntries() { - using DirectoryTreeEnumerator treeEnumerator = new(ioContext.DirectoryEntries, DirectoryEntry); + using DirectoryTreeEnumerator treeEnumerator = new(Context.DirectoryEntries, DirectoryEntry); while (treeEnumerator.MoveNext()) { yield return treeEnumerator.Current; @@ -44,7 +43,7 @@ IEnumerable EnumerateDirectoryEntries(StorageType type) => Enume DirectoryEntry AddDirectoryEntry(StorageType storageType, string name) { - DirectoryEntry entry = ioContext.DirectoryEntries.CreateOrRecycleDirectoryEntry(); + DirectoryEntry entry = Context.DirectoryEntries.CreateOrRecycleDirectoryEntry(); entry.Recycle(storageType, name); directoryTree.Add(entry); return entry; @@ -54,47 +53,47 @@ public Storage CreateStorage(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); DirectoryEntry entry = AddDirectoryEntry(StorageType.Storage, name); - return new Storage(ioContext, entry); + return new Storage(ContextSite, entry); } public CfbStream CreateStream(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); // TODO: Return a Stream that can transition between FAT and mini FAT DirectoryEntry entry = AddDirectoryEntry(StorageType.Stream, name); - return new CfbStream(ioContext, entry); + return new CfbStream(ContextSite, entry); } public Storage OpenStorage(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null || entry.Type is not StorageType.Storage) throw new DirectoryNotFoundException($"Storage not found: {name}."); - return new Storage(ioContext, entry); + return new Storage(ContextSite, entry); } public CfbStream OpenStream(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null || entry.Type is not StorageType.Stream) throw new FileNotFoundException($"Stream not found: {name}.", name); // TODO: Return a Stream that can transition between FAT and mini FAT - return new CfbStream(ioContext, entry); + return new CfbStream(ContextSite, entry); } public void CopyTo(Storage destination) @@ -103,13 +102,13 @@ public void CopyTo(Storage destination) { if (entry.Type is StorageType.Storage) { - Storage subSource = new(ioContext, entry); + Storage subSource = new(ContextSite, entry); Storage subDestination = destination.CreateStorage(entry.NameString); subSource.CopyTo(subDestination); } else if (entry.Type is StorageType.Stream) { - CfbStream stream = new(ioContext, entry); + CfbStream stream = new(ContextSite, entry); CfbStream destinationStream = destination.CreateStream(entry.NameString); stream.CopyTo(destinationStream); } @@ -120,7 +119,7 @@ public void Delete(string name) { ThrowHelper.ThrowIfNameIsInvalid(name); - this.ThrowIfDisposed(ioContext.IsDisposed); + this.ThrowIfDisposed(Context.IsDisposed); directoryTree.TryGetDirectoryEntry(name, out DirectoryEntry? entry); if (entry is null) @@ -128,7 +127,7 @@ public void Delete(string name) if (entry.Type is StorageType.Storage && entry.ChildId is not StreamId.NoStream) { - Storage storage = new(ioContext, entry); + Storage storage = new(ContextSite, entry); foreach (EntryInfo childEntry in storage.EnumerateEntries()) { storage.Delete(childEntry.Name); @@ -139,12 +138,12 @@ public void Delete(string name) { if (entry.StreamLength < Header.MiniStreamCutoffSize) { - using MiniFatChainEnumerator miniFatChainEnumerator = new(ioContext, entry.StartSectorId); + using MiniFatChainEnumerator miniFatChainEnumerator = new(ContextSite, entry.StartSectorId); miniFatChainEnumerator.Shrink(0); } else { - using FatChainEnumerator fatChainEnumerator = new(ioContext, entry.StartSectorId); + using FatChainEnumerator fatChainEnumerator = new(Context.Fat, entry.StartSectorId); fatChainEnumerator.Shrink(0); } } diff --git a/OpenMcdf/TransactedStream.cs b/OpenMcdf/TransactedStream.cs index dd616bee..5c01310a 100644 --- a/OpenMcdf/TransactedStream.cs +++ b/OpenMcdf/TransactedStream.cs @@ -4,12 +4,12 @@ namespace OpenMcdf; internal class TransactedStream : Stream { - readonly IOContext ioContext; + readonly RootContext ioContext; readonly Stream originalStream; readonly Dictionary dirtySectorPositions = new(); readonly byte[] buffer; - public TransactedStream(IOContext ioContext, Stream originalStream, Stream overlayStream) + public TransactedStream(RootContext ioContext, Stream originalStream, Stream overlayStream) { this.ioContext = ioContext; this.originalStream = originalStream; From 7159cd40860a2b771877800e00e6171121128613 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 16:46:41 +1300 Subject: [PATCH 105/114] Improve directory/header validation --- OpenMcdf/CfbBinaryReader.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/OpenMcdf/CfbBinaryReader.cs b/OpenMcdf/CfbBinaryReader.cs index 371ca7db..6079c400 100644 --- a/OpenMcdf/CfbBinaryReader.cs +++ b/OpenMcdf/CfbBinaryReader.cs @@ -45,6 +45,8 @@ public int Read(Span buffer) #endif + void ReadExactly(byte[] buffer, int offset, int count) => BaseStream.ReadExactly(buffer, offset, count); + public Guid ReadGuid() { int bytesRead = 0; @@ -68,8 +70,9 @@ public DateTime ReadFileTime() public Header ReadHeader() { Header header = new(); - Read(buffer, 0, Header.Signature.Length); - if (!buffer.Take(Header.Signature.Length).SequenceEqual(Header.Signature)) + ReadExactly(buffer, 0, Header.Signature.Length); + Span signature = buffer.AsSpan(0, Header.Signature.Length); + if (!signature.SequenceEqual(Header.Signature)) throw new FormatException("Invalid header signature."); header.CLSID = ReadGuid(); if (header.CLSID != Guid.Empty) @@ -127,7 +130,7 @@ public DirectoryEntry ReadDirectoryEntry(Version version, uint sid) if (version is not Version.V3 and not Version.V4) throw new ArgumentException($"Unsupported version: {version}.", nameof(version)); - Read(buffer, 0, DirectoryEntry.NameFieldLength); + ReadExactly(buffer, 0, DirectoryEntry.NameFieldLength); DirectoryEntry entry = new() { From 5d599c028a98fc0a3b0f663cf3db5de14e43a58a Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 20:37:17 +1300 Subject: [PATCH 106/114] Refactor RootContext --- OpenMcdf.Ole/OlePropertiesContainer.cs | 4 +-- OpenMcdf.Tests/StorageTests.cs | 2 -- OpenMcdf/DifatSectorEnumerator.cs | 4 --- OpenMcdf/DirectoryEntries.cs | 10 +++--- OpenMcdf/Fat.cs | 8 +---- OpenMcdf/FatSectorEnumerator.cs | 25 ++++----------- OpenMcdf/FatStream.cs | 10 +++--- OpenMcdf/MiniFatStream.cs | 10 +++--- OpenMcdf/RootContext.cs | 11 ++++++- OpenMcdf/RootStorage.cs | 4 +-- OpenMcdf/TransactedStream.cs | 44 ++++++++++++++------------ 11 files changed, 60 insertions(+), 72 deletions(-) diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index e47220f4..371730d0 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -1,6 +1,4 @@ -using OpenMcdf; - -namespace OpenMcdf.Ole; +namespace OpenMcdf.Ole; public enum ContainerType { diff --git a/OpenMcdf.Tests/StorageTests.cs b/OpenMcdf.Tests/StorageTests.cs index 1043e93c..c8c95488 100644 --- a/OpenMcdf.Tests/StorageTests.cs +++ b/OpenMcdf.Tests/StorageTests.cs @@ -1,7 +1,5 @@ namespace OpenMcdf.Tests; -using Version = OpenMcdf.Version; - [TestClass] public sealed class StorageTests { diff --git a/OpenMcdf/DifatSectorEnumerator.cs b/OpenMcdf/DifatSectorEnumerator.cs index c48baf4d..d4fa2d6d 100644 --- a/OpenMcdf/DifatSectorEnumerator.cs +++ b/OpenMcdf/DifatSectorEnumerator.cs @@ -1,11 +1,9 @@ using System.Collections; -using System.Diagnostics; namespace OpenMcdf; internal class DifatSectorEnumerator : ContextBase, IEnumerator { - public readonly uint DifatElementsPerSector; bool start = true; uint index = uint.MaxValue; Sector current = Sector.EndOfChain; @@ -14,13 +12,11 @@ internal class DifatSectorEnumerator : ContextBase, IEnumerator public DifatSectorEnumerator(RootContextSite rootContextSite) : base(rootContextSite) { - DifatElementsPerSector = (uint)((Context.SectorSize / sizeof(uint)) - 1); } /// public void Dispose() { - // IOContext is owned by parent } /// diff --git a/OpenMcdf/DirectoryEntries.cs b/OpenMcdf/DirectoryEntries.cs index 69ef5927..a510e9c1 100644 --- a/OpenMcdf/DirectoryEntries.cs +++ b/OpenMcdf/DirectoryEntries.cs @@ -4,14 +4,12 @@ internal sealed class DirectoryEntries : ContextBase, IDisposable { private readonly FatChainEnumerator fatChainEnumerator; private readonly DirectoryEntryEnumerator directoryEntryEnumerator; - private readonly int entriesPerSector; public DirectoryEntries(RootContextSite rootContextSite) : base(rootContextSite) { fatChainEnumerator = new FatChainEnumerator(Context.Fat, Context.Header.FirstDirectorySectorId); directoryEntryEnumerator = new DirectoryEntryEnumerator(this); - entriesPerSector = Context.SectorSize / DirectoryEntry.Length; } public void Dispose() @@ -40,7 +38,7 @@ public bool TryGetDictionaryEntry(uint streamId, out DirectoryEntry? entry) if (streamId > StreamId.Maximum) throw new ArgumentException($"Invalid directory entry stream ID: ${streamId:X8}.", nameof(streamId)); - uint chainIndex = (uint)Math.DivRem(streamId, entriesPerSector, out long entryIndex); + uint chainIndex = GetChainIndexAndEntryIndex(streamId, out long entryIndex); if (!fatChainEnumerator.MoveTo(chainIndex)) { entry = null; @@ -52,6 +50,8 @@ public bool TryGetDictionaryEntry(uint streamId, out DirectoryEntry? entry) return true; } + private uint GetChainIndexAndEntryIndex(uint streamId, out long entryIndex) => (uint)Math.DivRem(streamId, Context.DirectoryEntriesPerSector, out entryIndex); + public DirectoryEntry CreateOrRecycleDirectoryEntry() { DirectoryEntry? entry = TryRecycleDirectoryEntry(); @@ -68,7 +68,7 @@ public DirectoryEntry CreateOrRecycleDirectoryEntry() Sector sector = new(id, Context.SectorSize); writer.Position = sector.Position; - for (int i = 0; i < entriesPerSector; i++) + for (int i = 0; i < Context.DirectoryEntriesPerSector; i++) writer.Write(DirectoryEntry.Unallocated); entry = TryRecycleDirectoryEntry() @@ -92,7 +92,7 @@ public DirectoryEntry CreateOrRecycleDirectoryEntry() public void Write(DirectoryEntry entry) { - uint chainIndex = (uint)Math.DivRem(entry.Id, entriesPerSector, out long entryIndex); + uint chainIndex = GetChainIndexAndEntryIndex(entry.Id, out long entryIndex); if (!fatChainEnumerator.MoveTo(chainIndex)) throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); diff --git a/OpenMcdf/Fat.cs b/OpenMcdf/Fat.cs index 487bd74b..411c9c3f 100644 --- a/OpenMcdf/Fat.cs +++ b/OpenMcdf/Fat.cs @@ -10,7 +10,6 @@ namespace OpenMcdf; internal sealed class Fat : ContextBase, IEnumerable, IDisposable { private readonly FatSectorEnumerator fatSectorEnumerator; - internal readonly int FatElementsPerSector; private readonly byte[] cachedSectorBuffer; Sector cachedSector = Sector.EndOfChain; private bool isDirty; @@ -18,7 +17,6 @@ internal sealed class Fat : ContextBase, IEnumerable, IDisposable public Fat(RootContextSite rootContextSite) : base(rootContextSite) { - FatElementsPerSector = Context.SectorSize / sizeof(uint); fatSectorEnumerator = new(rootContextSite); cachedSectorBuffer = new byte[Context.SectorSize]; } @@ -46,11 +44,7 @@ public uint this[uint key] } } - uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) - { - uint index = (uint)Math.DivRem(key, FatElementsPerSector, out elementIndex); - return index; - } + uint GetSectorIndexAndElementOffset(uint key, out long elementIndex) => (uint)Math.DivRem(key, Context.FatEntriesPerSector, out elementIndex); void CacheCurrentSector() { diff --git a/OpenMcdf/FatSectorEnumerator.cs b/OpenMcdf/FatSectorEnumerator.cs index 9ab32d26..39a2a179 100644 --- a/OpenMcdf/FatSectorEnumerator.cs +++ b/OpenMcdf/FatSectorEnumerator.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Diagnostics; namespace OpenMcdf; @@ -87,8 +86,8 @@ public bool MoveTo(uint index) this.index = Header.DifatArrayLength; } - uint difatSectorIndex = (uint)Math.DivRem(index - Header.DifatArrayLength, difatSectorEnumerator.DifatElementsPerSector, out long difatElementIndex); - if (!difatSectorEnumerator.MoveTo(difatSectorIndex)) + uint difatChainIndex = GetDifatChainIndexAndDifatEntryIndex(index, out long difatElementIndex); + if (!difatSectorEnumerator.MoveTo(difatChainIndex)) { this.index = uint.MaxValue; current = Sector.EndOfChain; @@ -103,6 +102,9 @@ public bool MoveTo(uint index) return true; } + private uint GetDifatChainIndexAndDifatEntryIndex(uint index, out long difatElementIndex) + => (uint)Math.DivRem(index - Header.DifatArrayLength, Context.DifatEntriesPerSector, out difatElementIndex); + /// public void Reset() { @@ -112,21 +114,6 @@ public void Reset() difatSectorEnumerator.Reset(); } - (uint lastIndex, Sector lastSector) MoveToEnd() - { - Reset(); - - uint lastIndex = uint.MaxValue; - Sector lastSector = Sector.EndOfChain; - while (MoveNext()) - { - lastIndex = index; - lastSector = current; - } - - return (lastIndex, lastSector); - } - /// /// Extends the FAT by adding a new sector. /// @@ -154,7 +141,7 @@ public uint Add() } else { - uint difatSectorIndex = (uint)Math.DivRem(nextIndex - Header.DifatArrayLength, difatSectorEnumerator.DifatElementsPerSector, out long difatElementIndex); + uint difatSectorIndex = GetDifatChainIndexAndDifatEntryIndex(nextIndex, out long difatElementIndex); if (!difatSectorEnumerator.MoveTo(difatSectorIndex)) difatSectorEnumerator.Add(); diff --git a/OpenMcdf/FatStream.cs b/OpenMcdf/FatStream.cs index bede4309..871c164a 100644 --- a/OpenMcdf/FatStream.cs +++ b/OpenMcdf/FatStream.cs @@ -73,6 +73,8 @@ public override void Flush() Context.Writer!.Flush(); } + uint GetFatChainIndexAndSectorOffset(long offset, out long sectorOffset) => (uint)Math.DivRem(offset, Context.SectorSize, out sectorOffset); + /// public override int Read(byte[] buffer, int offset, int count) { @@ -87,7 +89,7 @@ public override int Read(byte[] buffer, int offset, int count) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); + uint chainIndex = GetFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; @@ -171,7 +173,7 @@ public override void Write(byte[] buffer, int offset, int count) if (count == 0) return; - uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); + uint chainIndex = GetFatChainIndexAndSectorOffset(position, out long sectorOffset); CfbBinaryWriter writer = Context.Writer; int writeCount = 0; @@ -220,7 +222,7 @@ public override int Read(Span buffer) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); + uint chainIndex = GetFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!chain.MoveTo(chainIndex)) return 0; @@ -257,7 +259,7 @@ public override void Write(ReadOnlySpan buffer) if (buffer.Length == 0) return; - uint chainIndex = (uint)Math.DivRem(position, Context.SectorSize, out long sectorOffset); + uint chainIndex = GetFatChainIndexAndSectorOffset(position, out long sectorOffset); CfbBinaryWriter writer = Context.Writer; int writeCount = 0; diff --git a/OpenMcdf/MiniFatStream.cs b/OpenMcdf/MiniFatStream.cs index 69699e02..2411feb4 100644 --- a/OpenMcdf/MiniFatStream.cs +++ b/OpenMcdf/MiniFatStream.cs @@ -64,6 +64,8 @@ public override void Flush() Context.MiniStream.Flush(); } + uint GetMiniFatChainIndexAndSectorOffset(long offset, out long sectorOffset) => (uint)Math.DivRem(offset, Context.MiniSectorSize, out sectorOffset); + public override int Read(byte[] buffer, int offset, int count) { ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); @@ -77,7 +79,7 @@ public override int Read(byte[] buffer, int offset, int count) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); + uint chainIndex = GetMiniFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) return 0; @@ -167,7 +169,7 @@ public override void Write(byte[] buffer, int offset, int count) if (position + count > ChainCapacity) SetLength(position + count); - uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); + uint chainIndex = GetMiniFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); @@ -212,7 +214,7 @@ public override int Read(Span buffer) if (maxCount == 0) return 0; - uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); + uint chainIndex = GetMiniFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) return 0; @@ -253,7 +255,7 @@ public override void Write(ReadOnlySpan buffer) if (position + buffer.Length > ChainCapacity) SetLength(position + buffer.Length); - uint chainIndex = (uint)Math.DivRem(position, Context.MiniSectorSize, out long sectorOffset); + uint chainIndex = GetMiniFatChainIndexAndSectorOffset(position, out long sectorOffset); if (!miniChain.MoveTo(chainIndex)) throw new InvalidOperationException($"Failed to move to mini FAT chain index: {chainIndex}."); diff --git a/OpenMcdf/RootContext.cs b/OpenMcdf/RootContext.cs index df72b3d0..f95addd8 100644 --- a/OpenMcdf/RootContext.cs +++ b/OpenMcdf/RootContext.cs @@ -70,6 +70,12 @@ public FatStream MiniStream public int MiniSectorSize { get; } + public int FatEntriesPerSector { get; } + + public int DifatEntriesPerSector { get; } + + public int DirectoryEntriesPerSector { get; } + public Version Version => (Version)Header.MajorVersion; public long Length { get; private set; } @@ -90,13 +96,16 @@ public RootContext(RootContextSite rootContextSite, Stream stream, Version versi Header = contextFlags.HasFlag(IOContextFlags.Create) ? new(version) : reader.ReadHeader(); SectorSize = 1 << Header.SectorShift; MiniSectorSize = 1 << Header.MiniSectorShift; + FatEntriesPerSector = SectorSize / sizeof(uint); + DifatEntriesPerSector = FatEntriesPerSector - 1; + DirectoryEntriesPerSector = SectorSize / DirectoryEntry.Length; Length = stream.Length; Stream actualStream = stream; if (contextFlags.HasFlag(IOContextFlags.Transacted)) { Stream overlayStream = stream is MemoryStream ? new MemoryStream() : File.Create(Path.GetTempFileName()); - transactedStream = new TransactedStream(this, stream, overlayStream); + transactedStream = new TransactedStream(ContextSite, stream, overlayStream); actualStream = new BufferedStream(transactedStream, SectorSize); } diff --git a/OpenMcdf/RootStorage.cs b/OpenMcdf/RootStorage.cs index 2f652008..86e2c3fd 100644 --- a/OpenMcdf/RootStorage.cs +++ b/OpenMcdf/RootStorage.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; - -namespace OpenMcdf; +namespace OpenMcdf; public enum Version : ushort { diff --git a/OpenMcdf/TransactedStream.cs b/OpenMcdf/TransactedStream.cs index 5c01310a..6b8d2a58 100644 --- a/OpenMcdf/TransactedStream.cs +++ b/OpenMcdf/TransactedStream.cs @@ -4,17 +4,19 @@ namespace OpenMcdf; internal class TransactedStream : Stream { - readonly RootContext ioContext; + readonly RootContextSite rootContextSite; readonly Stream originalStream; readonly Dictionary dirtySectorPositions = new(); readonly byte[] buffer; - public TransactedStream(RootContext ioContext, Stream originalStream, Stream overlayStream) + RootContext Context => rootContextSite.Context; + + public TransactedStream(RootContextSite rootContextSite, Stream originalStream, Stream overlayStream) { - this.ioContext = ioContext; + this.rootContextSite = rootContextSite; this.originalStream = originalStream; OverlayStream = overlayStream; - buffer = new byte[ioContext.SectorSize]; + buffer = new byte[Context.SectorSize]; } protected override void Dispose(bool disposing) @@ -39,6 +41,8 @@ protected override void Dispose(bool disposing) public override void Flush() => OverlayStream.Flush(); + uint GetFatChainIndexAndSectorOffset(long offset, out long sectorOffset) => (uint)Math.DivRem(offset, Context.SectorSize, out sectorOffset); + public override int Read(byte[] buffer, int offset, int count) { ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); @@ -47,8 +51,8 @@ public override int Read(byte[] buffer, int offset, int count) int totalRead = 0; do { - uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); - int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + uint sectorId = GetFatChainIndexAndSectorOffset(originalStream.Position, out long sectorOffset); + int remainingFromSector = Context.SectorSize - (int)sectorOffset; int localCount = Math.Min(count - totalRead, remainingFromSector); if (dirtySectorPositions.TryGetValue(sectorId, out long overlayPosition)) @@ -79,8 +83,8 @@ public override void Write(byte[] buffer, int offset, int count) { ThrowHelper.ThrowIfStreamArgumentsAreInvalid(buffer, offset, count); - uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); - int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + uint sectorId = GetFatChainIndexAndSectorOffset(originalStream.Position, out long sectorOffset); + int remainingFromSector = Context.SectorSize - (int)sectorOffset; int localCount = Math.Min(count, remainingFromSector); Debug.Assert(localCount == count); // TODO: Loop through the buffer and write to the overlay stream @@ -94,7 +98,7 @@ public override void Write(byte[] buffer, int offset, int count) } long originalPosition = originalStream.Position; - if (added && localCount != ioContext.SectorSize && originalPosition < originalStream.Length) + if (added && localCount != Context.SectorSize && originalPosition < originalStream.Length) { // Copy the existing sector data originalStream.Position = originalPosition - sectorOffset; @@ -106,8 +110,8 @@ public override void Write(byte[] buffer, int offset, int count) OverlayStream.Position = overlayPosition + sectorOffset; OverlayStream.Write(buffer, offset, localCount); - if (OverlayStream.Length < overlayPosition + ioContext.SectorSize) - OverlayStream.SetLength(overlayPosition + ioContext.SectorSize); + if (OverlayStream.Length < overlayPosition + Context.SectorSize) + OverlayStream.SetLength(overlayPosition + Context.SectorSize); originalStream.Position = originalPosition + localCount; } @@ -118,7 +122,7 @@ public void Commit() OverlayStream.Position = entry.Value; OverlayStream.ReadExactly(buffer); - originalStream.Position = entry.Key * ioContext.SectorSize; + originalStream.Position = entry.Key * Context.SectorSize; originalStream.Write(buffer, 0, buffer.Length); } @@ -137,8 +141,8 @@ public void Revert() public override int Read(Span buffer) { - uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); - int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + uint sectorId = (uint)Math.DivRem(originalStream.Position, Context.SectorSize, out long sectorOffset); + int remainingFromSector = Context.SectorSize - (int)sectorOffset; int localCount = Math.Min(buffer.Length, remainingFromSector); Debug.Assert(localCount == buffer.Length); @@ -162,8 +166,8 @@ public override int Read(Span buffer) public override void Write(ReadOnlySpan buffer) { - uint sectorId = (uint)Math.DivRem(originalStream.Position, ioContext.SectorSize, out long sectorOffset); - int remainingFromSector = ioContext.SectorSize - (int)sectorOffset; + uint sectorId = (uint)Math.DivRem(originalStream.Position, Context.SectorSize, out long sectorOffset); + int remainingFromSector = Context.SectorSize - (int)sectorOffset; int localCount = Math.Min(buffer.Length, remainingFromSector); Debug.Assert(localCount == buffer.Length); // TODO: Loop through the buffer and write to the overlay stream @@ -177,7 +181,7 @@ public override void Write(ReadOnlySpan buffer) } long originalPosition = originalStream.Position; - if (added && localCount != ioContext.SectorSize && originalPosition < originalStream.Length) + if (added && localCount != Context.SectorSize && originalPosition < originalStream.Length) { // Copy the existing sector data originalStream.Position = originalPosition - sectorOffset; @@ -189,10 +193,10 @@ public override void Write(ReadOnlySpan buffer) OverlayStream.Position = overlayPosition + sectorOffset; OverlayStream.Write(buffer); - if (OverlayStream.Length < overlayPosition + ioContext.SectorSize) - OverlayStream.SetLength(overlayPosition + ioContext.SectorSize); + if (OverlayStream.Length < overlayPosition + Context.SectorSize) + OverlayStream.SetLength(overlayPosition + Context.SectorSize); originalStream.Position = originalPosition + localCount; } #endif -} \ No newline at end of file +} From 2eae5e43ae9235b5e8512796a225ea3eeda7cfce Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 20:48:36 +1300 Subject: [PATCH 107/114] Improve comments --- OpenMcdf/ContextBase.cs | 2 +- OpenMcdf/DifatSectorEnumerator.cs | 3 +++ OpenMcdf/DirectoryEntries.cs | 3 +++ OpenMcdf/DirectoryEntryComparer.cs | 3 +++ OpenMcdf/DirectoryEntryEnumerator.cs | 2 +- OpenMcdf/DirectoryTree.cs | 5 ++++- OpenMcdf/FatEnumerator.cs | 2 +- OpenMcdf/FatSectorEnumerator.cs | 2 +- OpenMcdf/MiniFat.cs | 2 +- OpenMcdf/MiniFatChainEnumerator.cs | 2 +- OpenMcdf/MiniFatEnumerator.cs | 2 +- OpenMcdf/StreamExtensions.cs | 3 +++ OpenMcdf/TransactedStream.cs | 3 +++ 13 files changed, 26 insertions(+), 8 deletions(-) diff --git a/OpenMcdf/ContextBase.cs b/OpenMcdf/ContextBase.cs index ccf84c5d..28e6a12e 100644 --- a/OpenMcdf/ContextBase.cs +++ b/OpenMcdf/ContextBase.cs @@ -1,7 +1,7 @@ namespace OpenMcdf; /// -/// Supports switching the object. +/// Supports switching the object. /// public abstract class ContextBase { diff --git a/OpenMcdf/DifatSectorEnumerator.cs b/OpenMcdf/DifatSectorEnumerator.cs index d4fa2d6d..455e30b9 100644 --- a/OpenMcdf/DifatSectorEnumerator.cs +++ b/OpenMcdf/DifatSectorEnumerator.cs @@ -2,6 +2,9 @@ namespace OpenMcdf; +/// +/// Enumerates the s in a DIFAT chain. +/// internal class DifatSectorEnumerator : ContextBase, IEnumerator { bool start = true; diff --git a/OpenMcdf/DirectoryEntries.cs b/OpenMcdf/DirectoryEntries.cs index a510e9c1..da78abc2 100644 --- a/OpenMcdf/DirectoryEntries.cs +++ b/OpenMcdf/DirectoryEntries.cs @@ -1,5 +1,8 @@ namespace OpenMcdf; +/// +/// Encapsulates getting and adding objects." +/// internal sealed class DirectoryEntries : ContextBase, IDisposable { private readonly FatChainEnumerator fatChainEnumerator; diff --git a/OpenMcdf/DirectoryEntryComparer.cs b/OpenMcdf/DirectoryEntryComparer.cs index ae255374..d97a19b5 100644 --- a/OpenMcdf/DirectoryEntryComparer.cs +++ b/OpenMcdf/DirectoryEntryComparer.cs @@ -2,6 +2,9 @@ namespace OpenMcdf; +/// +/// Provides a for objects. +/// internal class DirectoryEntryComparer : IComparer { public static DirectoryEntryComparer Default { get; } = new(); diff --git a/OpenMcdf/DirectoryEntryEnumerator.cs b/OpenMcdf/DirectoryEntryEnumerator.cs index f80f78d1..b17ccffb 100644 --- a/OpenMcdf/DirectoryEntryEnumerator.cs +++ b/OpenMcdf/DirectoryEntryEnumerator.cs @@ -3,7 +3,7 @@ namespace OpenMcdf; /// -/// Enumerates instances from a . +/// Enumerates instances from a . /// internal sealed class DirectoryEntryEnumerator : IEnumerator { diff --git a/OpenMcdf/DirectoryTree.cs b/OpenMcdf/DirectoryTree.cs index 65b69ca5..1f0ba373 100644 --- a/OpenMcdf/DirectoryTree.cs +++ b/OpenMcdf/DirectoryTree.cs @@ -2,7 +2,10 @@ namespace OpenMcdf; -internal class DirectoryTree +/// +/// Encapsulates adding and removing objects to a tree. +/// +internal sealed class DirectoryTree { internal enum RelationType { diff --git a/OpenMcdf/FatEnumerator.cs b/OpenMcdf/FatEnumerator.cs index afeb1cd1..75190003 100644 --- a/OpenMcdf/FatEnumerator.cs +++ b/OpenMcdf/FatEnumerator.cs @@ -3,7 +3,7 @@ namespace OpenMcdf; /// -/// Enumerates the entries in a FAT. +/// Enumerates the records in a . /// internal class FatEnumerator : IEnumerator { diff --git a/OpenMcdf/FatSectorEnumerator.cs b/OpenMcdf/FatSectorEnumerator.cs index 39a2a179..6b00d4b8 100644 --- a/OpenMcdf/FatSectorEnumerator.cs +++ b/OpenMcdf/FatSectorEnumerator.cs @@ -3,7 +3,7 @@ namespace OpenMcdf; /// -/// Enumerates the FAT sectors of a compound file. +/// Enumerates the s of a compound file. /// internal sealed class FatSectorEnumerator : ContextBase, IEnumerator { diff --git a/OpenMcdf/MiniFat.cs b/OpenMcdf/MiniFat.cs index 8faa235c..3579660e 100644 --- a/OpenMcdf/MiniFat.cs +++ b/OpenMcdf/MiniFat.cs @@ -5,7 +5,7 @@ namespace OpenMcdf; /// -/// Encapsulates getting and setting entries in the mini FAT. +/// Encapsulates getting and setting records in the mini FAT. /// internal sealed class MiniFat : ContextBase, IEnumerable, IDisposable { diff --git a/OpenMcdf/MiniFatChainEnumerator.cs b/OpenMcdf/MiniFatChainEnumerator.cs index d6ff4cfc..fbf81e49 100644 --- a/OpenMcdf/MiniFatChainEnumerator.cs +++ b/OpenMcdf/MiniFatChainEnumerator.cs @@ -4,7 +4,7 @@ namespace OpenMcdf; /// -/// Enumerates the s in a Mini FAT sector chain. +/// Enumerates the s in a Mini FAT chain. /// internal sealed class MiniFatChainEnumerator : ContextBase, IEnumerator { diff --git a/OpenMcdf/MiniFatEnumerator.cs b/OpenMcdf/MiniFatEnumerator.cs index e5ad35f3..29d5be30 100644 --- a/OpenMcdf/MiniFatEnumerator.cs +++ b/OpenMcdf/MiniFatEnumerator.cs @@ -3,7 +3,7 @@ namespace OpenMcdf; /// -/// Enumerates the s from the Mini FAT. +/// Enumerates the s from the FAT chain for the mini FAT. /// internal sealed class MiniFatEnumerator : ContextBase, IEnumerator { diff --git a/OpenMcdf/StreamExtensions.cs b/OpenMcdf/StreamExtensions.cs index 5c6e2cb6..3d0941d3 100644 --- a/OpenMcdf/StreamExtensions.cs +++ b/OpenMcdf/StreamExtensions.cs @@ -4,6 +4,9 @@ namespace OpenMcdf; +/// +/// Adds modern extension methods to the class for netstandard2.0. +/// internal static class StreamExtensions { #if !NET7_0_OR_GREATER diff --git a/OpenMcdf/TransactedStream.cs b/OpenMcdf/TransactedStream.cs index 6b8d2a58..1abf31ac 100644 --- a/OpenMcdf/TransactedStream.cs +++ b/OpenMcdf/TransactedStream.cs @@ -2,6 +2,9 @@ namespace OpenMcdf; +/// +/// Stores modifications to a CFB stream that can be committed or reverted. +/// internal class TransactedStream : Stream { readonly RootContextSite rootContextSite; From 4395d2a23475e8512bbacdf75510f2ced01f1ac3 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 20:49:00 +1300 Subject: [PATCH 108/114] Optimize FatChainEnumerator --- OpenMcdf/DirectoryEntries.cs | 3 +-- OpenMcdf/FatChainEnumerator.cs | 36 +++++++++++++++++----------------- OpenMcdf/FatStream.cs | 2 +- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/OpenMcdf/DirectoryEntries.cs b/OpenMcdf/DirectoryEntries.cs index da78abc2..21a1d35b 100644 --- a/OpenMcdf/DirectoryEntries.cs +++ b/OpenMcdf/DirectoryEntries.cs @@ -100,8 +100,7 @@ public void Write(DirectoryEntry entry) throw new KeyNotFoundException($"Directory entry {entry.Id} was not found."); CfbBinaryWriter writer = Context.Writer; - Sector sector = new(fatChainEnumerator.Current.Value, Context.SectorSize); - writer.Position = sector.Position + (entryIndex * DirectoryEntry.Length); + writer.Position = fatChainEnumerator.CurrentSector.Position + (entryIndex * DirectoryEntry.Length); writer.Write(entry); } } diff --git a/OpenMcdf/FatChainEnumerator.cs b/OpenMcdf/FatChainEnumerator.cs index a0725859..b89bf78b 100644 --- a/OpenMcdf/FatChainEnumerator.cs +++ b/OpenMcdf/FatChainEnumerator.cs @@ -6,14 +6,14 @@ namespace OpenMcdf; /// /// Enumerates the s in a FAT sector chain. /// -internal sealed class FatChainEnumerator : IEnumerator +internal sealed class FatChainEnumerator : IEnumerator { private readonly Fat fat; private readonly FatEnumerator fatEnumerator; private uint startId; private bool start = true; private uint index = uint.MaxValue; - private FatChainEntry current = FatChainEntry.Invalid; + private uint current = uint.MaxValue; private long length = -1; public FatChainEnumerator(Fat fat, uint startSectorId) @@ -29,10 +29,10 @@ public void Dispose() fatEnumerator.Dispose(); } - public Sector CurrentSector => new(Current.Value, fat.Context.SectorSize); + public Sector CurrentSector => new(current, fat.Context.SectorSize); /// - public FatChainEntry Current + public uint Current { get { @@ -55,28 +55,28 @@ public bool MoveNext() if (startId is SectorType.EndOfChain or SectorType.Free) { index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; return false; } index = 0; - current = new(index, startId); + current = startId; start = false; return true; } - if (current.IsFreeOrEndOfChain || current == FatChainEntry.Invalid) + if (SectorType.IsFreeOrEndOfChain(current)) { index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; return false; } - uint value = fat[current.Value]; + uint value = fat[current]; if (value is SectorType.EndOfChain) { index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; return false; } @@ -85,11 +85,11 @@ public bool MoveNext() { // If the index is greater than the maximum, then the chain must contain a loop index = uint.MaxValue; - current = FatChainEntry.Invalid; - throw new IOException("FAT sector chain is corrupt"); + current = uint.MaxValue; + throw new IOException("FAT sector chain is corrupt."); } - current = new(index, value); + current = value; return true; } @@ -154,7 +154,7 @@ public uint Extend(uint requiredChainLength) bool ok = MoveTo(chainLength - 1); Debug.Assert(ok); - uint lastId = current.Value; + uint lastId = current; ok = fatEnumerator.MoveTo(lastId); Debug.Assert(ok); while (chainLength < requiredChainLength) @@ -180,7 +180,7 @@ public uint ExtendFrom(uint hintId) uint lastId = startId; while (MoveNext()) { - lastId = current.Value; + lastId = current; } uint id = fat.Add(fatEnumerator, lastId); @@ -196,7 +196,7 @@ public uint Shrink(uint requiredChainLength) Reset(); - uint lastId = current.Value; + uint lastId = current; while (MoveNext()) { if (lastId is not SectorType.EndOfChain and not SectorType.Free) @@ -207,7 +207,7 @@ public uint Shrink(uint requiredChainLength) fat[lastId] = SectorType.Free; } - lastId = current.Value; + lastId = current; } fat[lastId] = SectorType.Free; @@ -235,7 +235,7 @@ public void Reset(uint startSectorId) startId = startSectorId; start = true; index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; } public override string ToString() => $"{current}"; diff --git a/OpenMcdf/FatStream.cs b/OpenMcdf/FatStream.cs index 871c164a..bf4cbd1a 100644 --- a/OpenMcdf/FatStream.cs +++ b/OpenMcdf/FatStream.cs @@ -97,7 +97,7 @@ public override int Read(byte[] buffer, int offset, int count) int readCount = 0; do { - Sector sector = new(chain.Current.Value, Context.SectorSize); + Sector sector = chain.CurrentSector; int remaining = realCount - readCount; long readLength = Math.Min(remaining, sector.Length - sectorOffset); Context.Reader.Position = sector.Position + sectorOffset; From 821cde4a005906494abaf37b0e50fb5c4175933d Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 20:52:40 +1300 Subject: [PATCH 109/114] Optimize MiniFatChainEnumerator --- OpenMcdf/FatChainEntry.cs | 10 ------- OpenMcdf/FatChainEnumerator.cs | 2 +- OpenMcdf/MiniFatChainEnumerator.cs | 44 ++++++++++++++---------------- OpenMcdf/MiniFatStream.cs | 5 ++-- 4 files changed, 24 insertions(+), 37 deletions(-) delete mode 100644 OpenMcdf/FatChainEntry.cs diff --git a/OpenMcdf/FatChainEntry.cs b/OpenMcdf/FatChainEntry.cs deleted file mode 100644 index 0f5fdde8..00000000 --- a/OpenMcdf/FatChainEntry.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenMcdf; - -internal record struct FatChainEntry(uint Index, uint Value) -{ - internal static readonly FatChainEntry Invalid = new(uint.MaxValue, SectorType.EndOfChain); - - public readonly bool IsFreeOrEndOfChain => SectorType.IsFreeOrEndOfChain(Value); - - public override readonly string ToString() => $"#{Index}: {Value}"; -} diff --git a/OpenMcdf/FatChainEnumerator.cs b/OpenMcdf/FatChainEnumerator.cs index b89bf78b..2ca194d3 100644 --- a/OpenMcdf/FatChainEnumerator.cs +++ b/OpenMcdf/FatChainEnumerator.cs @@ -238,5 +238,5 @@ public void Reset(uint startSectorId) current = uint.MaxValue; } - public override string ToString() => $"{current}"; + public override string ToString() => $"Index: {index} Current: {current}"; } diff --git a/OpenMcdf/MiniFatChainEnumerator.cs b/OpenMcdf/MiniFatChainEnumerator.cs index fbf81e49..48e59aa4 100644 --- a/OpenMcdf/MiniFatChainEnumerator.cs +++ b/OpenMcdf/MiniFatChainEnumerator.cs @@ -6,13 +6,13 @@ namespace OpenMcdf; /// /// Enumerates the s in a Mini FAT chain. /// -internal sealed class MiniFatChainEnumerator : ContextBase, IEnumerator +internal sealed class MiniFatChainEnumerator : ContextBase, IEnumerator { private readonly MiniFatEnumerator miniFatEnumerator; private uint startId; private bool start = true; uint index = uint.MaxValue; - private FatChainEntry current = FatChainEntry.Invalid; + private uint current = uint.MaxValue; private long length = -1; public MiniFatChainEnumerator(RootContextSite rootContextSite, uint startSectorId) @@ -31,18 +31,14 @@ public void Dispose() /// The index within the Mini FAT sector chain, or if the enumeration has not started. /// - public uint StartId => startId; - - public uint Index => index; - - public MiniSector CurrentSector => new(Current.Value, Context.MiniSectorSize); + public MiniSector CurrentSector => new(Current, Context.MiniSectorSize); /// - public FatChainEntry Current + public uint Current { get { - if (current.IsFreeOrEndOfChain) + if (index == uint.MaxValue) throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); return current; } @@ -58,15 +54,15 @@ public bool MoveNext() { start = false; index = 0; - current = new(index, startId); + current = startId; } - else if (!current.IsFreeOrEndOfChain) + else if (!SectorType.IsFreeOrEndOfChain(current)) { - uint sectorId = Context.MiniFat[current.Value]; + uint sectorId = Context.MiniFat[current]; if (sectorId == SectorType.EndOfChain) { index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; return false; } @@ -75,14 +71,14 @@ public bool MoveNext() throw new FormatException("Mini FAT chain is corrupt."); index = nextIndex; - current = new(nextIndex, sectorId); + current = sectorId; return true; } - if (current.IsFreeOrEndOfChain) + if (SectorType.IsFreeOrEndOfChain(current)) { index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; return false; } @@ -123,7 +119,7 @@ public long GetLength() return length; } - public void Extend(uint requiredChainLength) + public uint Extend(uint requiredChainLength) { uint chainLength = (uint)GetLength(); if (chainLength >= requiredChainLength) @@ -138,7 +134,7 @@ public void Extend(uint requiredChainLength) bool ok = MoveTo(chainLength - 1); Debug.Assert(ok); - uint lastId = current.Value; + uint lastId = current; ok = miniFatEnumerator.MoveTo(lastId); Debug.Assert(ok); while (chainLength < requiredChainLength) @@ -156,9 +152,10 @@ public void Extend(uint requiredChainLength) #endif length = requiredChainLength; + return startId; } - public void Shrink(uint requiredChainLength) + public uint Shrink(uint requiredChainLength) { uint chainLength = (uint)GetLength(); if (chainLength <= requiredChainLength) @@ -166,7 +163,7 @@ public void Shrink(uint requiredChainLength) Reset(); - uint lastId = current.Value; + uint lastId = current; while (MoveNext()) { if (lastId <= SectorType.Maximum) @@ -177,7 +174,7 @@ public void Shrink(uint requiredChainLength) Context.MiniFat[lastId] = SectorType.Free; } - lastId = current.Value; + lastId = current; } if (lastId <= SectorType.Maximum) @@ -195,6 +192,7 @@ public void Shrink(uint requiredChainLength) #endif length = requiredChainLength; + return startId; } /// @@ -202,8 +200,8 @@ public void Reset() { start = true; index = uint.MaxValue; - current = FatChainEntry.Invalid; + current = uint.MaxValue; } - public override string ToString() => $"Index: {index} Value {current}"; + public override string ToString() => $"Index: {index} Current: {current}"; } diff --git a/OpenMcdf/MiniFatStream.cs b/OpenMcdf/MiniFatStream.cs index 2411feb4..d1c92770 100644 --- a/OpenMcdf/MiniFatStream.cs +++ b/OpenMcdf/MiniFatStream.cs @@ -147,11 +147,10 @@ public override void SetLength(long value) uint requiredChainLength = (uint)((value + Context.MiniSectorSize - 1) / Context.MiniSectorSize); if (value > ChainCapacity) - miniChain.Extend(requiredChainLength); + DirectoryEntry.StartSectorId = miniChain.Extend(requiredChainLength); else if (value <= ChainCapacity - Context.MiniSectorSize) - miniChain.Shrink(requiredChainLength); + DirectoryEntry.StartSectorId = miniChain.Shrink(requiredChainLength); - DirectoryEntry.StartSectorId = miniChain.StartId; DirectoryEntry.StreamLength = value; isDirty = true; } From 94ad220ade54c5449f47e1098f52219415fd3ed0 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 21:38:25 +1300 Subject: [PATCH 110/114] Seal StreamDataProvider --- StructuredStorageExplorer/StreamDataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StructuredStorageExplorer/StreamDataProvider.cs b/StructuredStorageExplorer/StreamDataProvider.cs index e99c23b7..d2973128 100644 --- a/StructuredStorageExplorer/StreamDataProvider.cs +++ b/StructuredStorageExplorer/StreamDataProvider.cs @@ -3,7 +3,7 @@ namespace StructuredStorageExplorer; -public class StreamDataProvider : IByteProvider +internal sealed class StreamDataProvider : IByteProvider { /// /// Modifying stream From c9b8d2385c2d362857e740e481a27ea224357ec7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 23:15:32 +1300 Subject: [PATCH 111/114] Fix OLE stack overflow --- OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs index 551e55ed..828dbfa6 100644 --- a/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs +++ b/OpenMcdf.Ole/DocumentSummaryInfoPropertyFactory.cs @@ -11,6 +11,6 @@ protected override ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, if (propertyIdentifier is 0x0000000C or 0x0000000D) return new VT_Unaligned_LPSTR_Property(vType, codePage, isVariant); - return CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); + return base.CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); } } From 7ddcf4e484271900e27bfc71305c3c5b34176bf7 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 23:36:06 +1300 Subject: [PATCH 112/114] Fix OLE BinaryReader/Writer usage --- OpenMcdf.Ole/OlePropertiesContainer.cs | 7 ++++--- OpenMcdf.Ole/PropertySetStream.cs | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index 371730d0..f21fea91 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf.Ole; +using System.Text; + +namespace OpenMcdf.Ole; public enum ContainerType { @@ -42,8 +44,7 @@ public OlePropertiesContainer(CfbStream cfStream) this.cfStream = cfStream; - cfStream.Position = 0; - using BinaryReader reader = new(cfStream); + using BinaryReader reader = new(cfStream, Encoding.Unicode, true); pStream.Read(reader); if (pStream.FMTID0 == FormatIdentifiers.SummaryInformation) diff --git a/OpenMcdf.Ole/PropertySetStream.cs b/OpenMcdf.Ole/PropertySetStream.cs index 2f3de824..57caf285 100644 --- a/OpenMcdf.Ole/PropertySetStream.cs +++ b/OpenMcdf.Ole/PropertySetStream.cs @@ -28,6 +28,8 @@ public PropertySetStream() public void Read(BinaryReader br) { + br.BaseStream.Position = 0; + ByteOrder = br.ReadUInt16(); Version = br.ReadUInt16(); SystemIdentifier = br.ReadUInt32(); @@ -113,6 +115,8 @@ public void Read(BinaryReader br) public void Write(BinaryWriter bw) { + bw.BaseStream.Position = 0; + OffsetContainer oc0 = new(); OffsetContainer oc1 = new(); From 502926cfeb90e0a103e2e1d1a8054e738618a072 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 23:45:59 +1300 Subject: [PATCH 113/114] Fix encoding provider registration --- OpenMcdf.Ole/CodePages.cs | 4 +++- OpenMcdf.Ole/PropertyFactory.cs | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenMcdf.Ole/CodePages.cs b/OpenMcdf.Ole/CodePages.cs index 1f5a22c7..fdf34886 100644 --- a/OpenMcdf.Ole/CodePages.cs +++ b/OpenMcdf.Ole/CodePages.cs @@ -1,4 +1,6 @@ -namespace OpenMcdf.Ole; +using System.Text; + +namespace OpenMcdf.Ole; internal static class CodePages { diff --git a/OpenMcdf.Ole/PropertyFactory.cs b/OpenMcdf.Ole/PropertyFactory.cs index f8a699f1..9a1df14a 100644 --- a/OpenMcdf.Ole/PropertyFactory.cs +++ b/OpenMcdf.Ole/PropertyFactory.cs @@ -6,9 +6,7 @@ internal abstract class PropertyFactory { static PropertyFactory() { -#if NETSTANDARD2_0_OR_GREATER Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); -#endif } public ITypedPropertyValue CreateProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant = false) From f454dac12c1ce2c1d33a8519bebb3510d4fe3080 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 12 Nov 2024 23:11:17 +1300 Subject: [PATCH 114/114] Add OLE tests --- OpenMcdf.Ole.Tests/2custom.doc | Bin 0 -> 27136 bytes OpenMcdf.Ole.Tests/CLSIDPropertyTest.file | Bin 0 -> 245760 bytes OpenMcdf.Ole.Tests/Issue134.cfs | Bin 0 -> 2560 bytes .../OlePropertiesExtensionsTests.cs | 440 ++++++++++++++++++ OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj | 54 +++ OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls | Bin 0 -> 19456 bytes OpenMcdf.Ole.Tests/_Test.ppt | Bin 0 -> 174592 bytes OpenMcdf.Ole.Tests/english.presets.doc | Bin 0 -> 9728 bytes OpenMcdf.Ole.Tests/report.xls | Bin 0 -> 16896 bytes OpenMcdf.Ole.Tests/winUnicodeDictionary.doc | Bin 0 -> 26624 bytes OpenMcdf.Ole.Tests/wstr_presets.doc | Bin 0 -> 27136 bytes OpenMcdf.Tests/OpenMcdf.Tests.csproj | 2 +- OpenMcdf.sln | 6 + OpenMcdf/RootStorage.cs | 4 +- 14 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 OpenMcdf.Ole.Tests/2custom.doc create mode 100644 OpenMcdf.Ole.Tests/CLSIDPropertyTest.file create mode 100644 OpenMcdf.Ole.Tests/Issue134.cfs create mode 100644 OpenMcdf.Ole.Tests/OlePropertiesExtensionsTests.cs create mode 100644 OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj create mode 100644 OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls create mode 100644 OpenMcdf.Ole.Tests/_Test.ppt create mode 100644 OpenMcdf.Ole.Tests/english.presets.doc create mode 100644 OpenMcdf.Ole.Tests/report.xls create mode 100644 OpenMcdf.Ole.Tests/winUnicodeDictionary.doc create mode 100644 OpenMcdf.Ole.Tests/wstr_presets.doc diff --git a/OpenMcdf.Ole.Tests/2custom.doc b/OpenMcdf.Ole.Tests/2custom.doc new file mode 100644 index 0000000000000000000000000000000000000000..0d53d3a1ae713c9cf17f3e8cfe094ced38e54ee0 GIT binary patch literal 27136 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 RZGl*zM3RiRYX5WB{{xX)F>C+; literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/CLSIDPropertyTest.file b/OpenMcdf.Ole.Tests/CLSIDPropertyTest.file new file mode 100644 index 0000000000000000000000000000000000000000..27b609bf61b6e6e31b5186e8d59f95ef3b3ff142 GIT binary patch literal 245760 zcmeEv2Vhji_V;XPQY8@;dqhxF+@X2n?j@Kb-MI= zUjM|Mx4@t9+--!HSUSq9a(zsd_tQ%`Zohbv=HZyu3u*I^L7&fjJ_F|U#|QnrKaen1 z1@e<)JrxA!A#8n)5JM2Ydht`|$KZ8qeWKHzABg4uSQz1qLD|9HV9?MhX4dP^1tzcM(J7I5MF`0+R*n zZVeoVXCu5{jHQ0&G5uWNK%{egYV`%nI>I1_8i}0eh`}fm4>>7)AZi|fG|I%hP)J0I zXk8wrP;mj!QD$G%NWJJ4`00xloeRBLTYY(sWgB{KEPrR^|Bf{0mitd?V_HD(OZypY z>oH|}<6&Fz7y)WL^?{wa3!O1?^>VmF_azF^hY$i$XM!BlVU1jQDEf}VVBi?yI`d|g zROi!oTdsfH>w66yFDsCtzCZ|k${1iM=G|bcx6qfUKi&!TUyLG`1KAGFN{8%>GFQH~ z*Pw;^G9l7un9v5nXbFy{K&sPUu$c2p4gpa zKYuxvI_(lscybBz<{%)Cc|h9#vp|li#lRN8Bp}DnN+8!|slao989>^9 z0g&saO~4Mo8X(tV`+?-^I2Ktqfn!jM%l#;x8yo6*=;J5Ai#iax16fZnAD|}Jyp)T+ zgXxU70|sKOhQa@t;1v?&XijkHPl8askGR3|?^aOs!>;@$d=5gq5Bybcc$xv=*&i#! zo5d~o7X-Z3N~6)b3f(UWQQQK}ehxqO&iB8Ln%?pK7hylwe6)vc;F#76Y3Vg8G6W%QB^n`ZWgbG>%0h%(Z@-0*ZNCyB z?J^A^ZKwbtZDlVp%3)8hVl&i5c7-iUJ^ggVY{Sb;-ihH#P zXdHx3?u-Z;4<=HdAMw!_A=jOJu0#RDt>}x1PgXtf#~Y=F9!MlZcD8e-Ly$fm%{vxq z-6R)+>@dkF0E|v`DSxUewP}nft;VR6*MCfZO_*2KD2e)Gfk5@xJc;2@?OGwukF4H! z+j(D;Q5$K?wAbmfy~6Q16Q@61k7!Eu!MN{>9;Po)W5UX4-Sn9gJ2jCJjID9A#XqJ0 z*IGbXWGF21yv`TBxai(Z(zH3yi1)&q%mzkdwQiK-(`J*Li#THGk)<_G5h5b&#JGKr zyr7u4J7Tr*L61t5^-j>GMYHk;PXNg8F6Ztj4&rn-}lj9=Op*3ghXxJo!bt4(6LCYnWWHbiLsGG*OeA#b7O_La%uQ7rPnR&ABxEZ0(Zfz1H9^y!eF!Rm4 z;@gh9t2Q)^5l@07qd*~Iw)F5ha#c!glO*b_F~XWYX6c67Qu$UFg^gWyG>H*U+9jhw*pYTxxn{;^-`E(@>p5R# zg6e@+l@!tejdkaclsZ-ePiz)_ldiMc=xRQKV*Y#dDSF$Po3RtFzgC!#2?(1{uqyp zIZ3$F+Sh^jvq{T(%bNXHtysAQ#22|qd_9PNZNB(YNnCMk${)2LHn>U5z1f$G%4SL8 zu#_IJZ)*hc4IoarXaU3;cDJjz@3z)l5_oc9Gmu%iyJHITo(00dlMq51>XDIouQ`#i&)+Dc7i3&kkBrP)4#bi}*N%O0s;1g8PM)Bgy>>*`S%Lk}qYByo zQ-ru4av9|5lq%9r#*rvX7*K*It_HkWQDoz)iq%;2|JouC#=VS0i2n+zPDKrSCv| zCvX>Vw=R7j;`@OIfP9ll9)}Sg0e%ep1jw{c5q<{z-17W|C1l>$h#v=j1N;_v0{9*9 zd*BbiAAu)%1WSscB{u%MVfbi-ecO1Ro6GLz=xfpG|NgNix z;B3CL;bH?%=Es$mZ}Pr;mLC~Gmi*N{gYG-gEyzE+TlcTK2fpc-W1d>{@4yQUiR*vA@#%*y^l#Jikvo%W_xrW)d_42reV_WZJMo!U&-PdNFCQ~={{z{l z{IXuUeDy~cck_R={Ds2Gu(bRi`<|Tn;>d0JVZE+Q_KEz)@0a|UM|W;qn_s=)kC#@N zzww*Ba@y2_-)sG@Jv`&Q-nHN6Kba8SHf-hc{5My>df%*)jQsV#B(1)^^a}q6)_wKF zD}68VU%qXd*Xmyk{!6>OzB)K(pP%<{U6wt$YLDNyPuy!-^Y9jnp464?b*6yr-xAD5 zkH!YaSNU|_HkyW0H{Jt`$GmRB`P)=krmZlbOmnfY_4k*xF0AtY;1F+XecizesZ+eS z4OsOtZeCu3w5NJ_HAC9BYsWM%nKyg*vWsrZHV?ipra97j-B~-g`1;kOGQNBKyH`H# z*QEv0MvrbM$U}h_0v_wM;G_P-&!kY=zWj%yRhfA+`LNa8Z9=?QnPT)d#$+i?2VQDEA)~95jN=xvt{6h3kUG zKw~OD_TtdOUJJr!eDc8Z5053hy?V!#_vgpQ#+n*apJQ3lr$T1-*JSJ%6Jaf*U_I&< ziBaAv;Ty*{PzDx;GJ(>%c;AONPMG%+@;b`AP|$O8&38}a69dbeYc2OE%C$qe3815m z`r@rq7|QVGD*0W3e9EEjC&Xa~Ys8IK@|kMAtuP(=EE9^n7}U<19QjN}KI<6^X?QQf zG5;Rq@hD`V)K;BP&qXPv8Se`6CXKqwAnMc;H{w8J4>(`{Uc;>d^ho5MlamCW% zaX%#|rVSo5dtBg@?M1Dh9{yp!f`^uct!Od+p@qLbxnfrJ{pXckSN)P z%tse=xPM0cbxTeaFMn*(u;1Uf$CN+Hd&W!4Zo2!bl4DPm$98*p;;TQE4ILI1e)H-X zb7pV&{Dm79t$O#$F^}FI^1$Uq$6lKMz%|Q0y)SB6ublqx_&$H5>EgK|i+rAlNpCj% z)k%Mb-ac#j3&9%-NBung+DGqwxL2b0M-ln^m-NXfE!&a%>WsH$FIW(k@$UuIMVE}I zSP=5=d9#+UKUq59fz+{mAK(4zM>$*bZ(BAn_PwMF-!1xJ{GxxA{k0-^Oio7nx*sDR zUY|0{C%MmKZ$!SaYQTyq%NOUDJ=QAs>nUB{jg0TPtZVSiPrje_>V(b{mgLMDDq?z% zYmtBGsU>eMTyEG9IqR;HYciIH{8oG@XYq``-g@u3gtV;>-<)*mI#WXU>kD$e{C7(5 z@)vicCVmil|1&S#nwLKRMChR97v$%TuQI(Eqt+VAdJICXXkD#fRjCvC`W0(}DQqg& zZ&w-sO`zur=lYlCOV-+ox-s>;vtkapJZBCoSUb4rl|LRvB;fL3KD$~0pQ#l#>mayV zQENK}S33hmZU5O5r(|7$Gl4NcwmqGN2`h;>gj^Slm2qErf0A>%_FV!KTpL#tydlHe z{=8+u^$mDh)|`mwps$1?-O68=StokJTCQzChhdAO*83d}njG-P+6t9k@VvhyS@?+m z{`mKbM*sM(dMkn6oHeDau8CT4)2z`M=Wt`tMQLDmmr1H*g zV{A^-N$Vr+8SI_HfW8ZZR?Pi4-xB(1VfKR8E8Dl)=(oLcL-?LMcDvn2tx-z^WY%k; zUZe45zCP7`-PCBh^tG>l{_T!+ex}5iUrFn}DSz>AKg(yQYKgepL)v`syx(Pc`A?+# zt^WALHoqko54mS-k8k`2C0_e&hwyB_A1|ETclguWm1ap#`b$0-O{GOY4!gl;m3mH^ zV@Z$q=Wa*!i(U(LD{ib@#p}}>tKF|jw`Wg#vefU?;=%L1ZpzC~-MgvP&*!Bp>BM3l zH38<`ZV7YlPw9Hw?yBwincqB))cXy>Z*RSx&wo5O%bY~sy z^1a&4>=C!ZU-HpucZXsu_|B9NZcu)ZJ2HAVuEsR~%6ud8>0idfdby#X?%YUda}LW) zv+&^C$PwZ$xe(LlBIZZhnroL(<|mB zTJna2C!*H*mwB_Sd3|J_cFM)P+19+iGEaZOnWC zl!V$TB&i;9Np-h_gxZ-UsUCKNN=c}laFS}C8&pa{?L2fe;2cn9cjj4y8cBTDMBoX< z0O?t9y5SO5mJYx9semLu$IvAKGI+IsSYMg?@*MA&JAP-?@jDET-w7J?*_6xr&e`Uj zafc`vHFw=c$L}}PS2y!;ei-U6M@vH`o#(%J*RjT|PPZ||t42+MOWt=oHATTgs>_fdLjf;u8FVPq{VA8l=z54c_9MCf^VJ7bN ze&DoCxen5opv;nb%b0w{X4RiFy+wb-G&qs3#Z=gk zdRl3JizY65Q!A!d6KSm9^1WWjr9kT!DnMW-YjL)O7H2<`-g17|WO}fWcL1(p1aKj2 zLpu|!&wjM_qdXjxnw*Slax$*T$#@}<^)T+N9;QEz4wHR`INO401Wl$E>QV0H7}eTC zPs8+<{1C#Q@0e)sTAc6twD_CWxcaV~O2?fCRxD|8$3exV-SBRgzJEyGRJ}>7wJ+nR zqpPJ&{N2;_9FfclE&B00f+}v1cg9Xfj*jv!ub2nc&HM#=%(tVwvtqDI2|4mylfkShw(_3m>rcjDr7AdX+cY`j)%NGgFI)aAvll~k~EBaP$ zanRHAT_l^)BELUt_61Ms-vj9eUm*&-I$Ba5M~Z#@Eg?1S)k2Tb^Q@&CqGuf+gBBG^ zjlc&f?I2EbOA4$5DM3hSp(}&ZvQCD8dGZqnC2gK1tI_nN zt9J~~6aLs!dvOf2F3Qpl*wGT#p7989*IzfjoO0cJ9OegAE9<2kr!#gy@6T_u(m&y) z;ykv=ncznsM}F+H)8PjTfqaj8(O+o&&>lSX{lI4hjc;F#(s^j(cW>{gZM&8pIb;3c z!LPpmQ5`K{@pp1;(Dt-($Ta}R4OgD0Gj23~Tt|=}eb?#m1Nl5ksjWr8UjoM@LGPRS z?8>*YoF*H6D`n%F?sR0+`2Dx~Fik$Lf2c3_MyI1M$o<)h^ogg#N0W>9@+lYJ+?)=- zP@G1o+gY?WS~xYOC1^RQ2wvP@eEBS6ETQ>y+J#msLC&=G(Bx&KMfRsFa3$C4 z-)6t>$ct8{=QU(a&KuwFx;t;knn`bdzssDb@tU!D@(k>S{Hh-sTK)R#<$lk!`Ro}- zUi2P%UXJqhlgzZ+1h)3NWB8Ufj=ad{DPY#yKi^UN{6TAbg}gB9v$>k`f{i`&ND$BkX zF@kLkrJh}Vp80ZI7izx~W`82c?-9-2*NdQ!`|jg{7( zm#e47>vqe72ZulZ{I>jv34erNI(n(!?B5P$^x2i|q`mv$_Yz#o%vQIYvz&FA8`>Y~ z+gY<~zQ>No+c5oD8I7dt+$|JE5f+qYSLKDRYWTh6(~!Chq{{JkT7IjcjKB(8io z=je64eQ#U)rNxg6F*y3_7p_~r(E3WNSNC_yZX0%&6@VDJ!fHwLF#I+k1{7v%+>o;`tH!Ko$H!S#<0Vd(Dh3$q# z7;Z4D@2qol6aJps)u^@Au8o7gt9H)cs&>xb^v#67t9Fh)o9-NMguknHjw4&`n&2X|Jhnz+j#w5wcli^UFpvC zCgJa@-QMQuZE@?feH&ljF5_`OE-E&r%dNg$diU(rrH4!$5|%LCX~w|m#_$kAyzyKn!XoBI#wb<@CG zdIb&$8q{wGu9Vz4m`=aQrT?J1dPF z<*(z|JdYfF`1oh6Nni7io9NOs50|S=VLZ~+jOw@+X*jIoKmKv6q3FX(lYUwL341RV&O(LMZp{2)PsC!2#EXH9+o-_XA1u3D5`lHShx9 zkH8Coe0|dy0dH+xgs?M0evh{+!b=eLLde^z{QZBz5KE|@)F*cV`rqdtf@jCSGj;{i zD5vdv4mkV3Gk48&Kdgg44#SXD$Q!c=NvBnLI&pvSsoi9hzY58AqRiB9B<5g#A5Z(9 z@;`s`O#PmU_BYSIJGxbT`}slf&6l5u-%DqU?+=xTZ;l@q-ybeT`VsM4*@JlgO8m0< z5%FvJeDTAVrui68c5ij%um;(3KQxpO7T zJSTqIzFeH#y;^*~KLdFmh+nFnN0=gh-2I{WX~#10!=5!L_k#GP`X%JOguM4b`@Z;b zZ#wv;BYmOxx%y?~EfhbZ>`yhXATJH^wcr8VvkuSe#MH3K;^ekulzmkgC;eMY`OhTr zbIl_0^H$*YB!r8_&$Vxe&=JAnuF<2#-D5+=kbwhH=5^!|-xNR9z9oi1_nA@C!0RnB zY1~+(y(1R)=`BVK9wf$(3K4%COc9gro+$qODo0G3bdQLhK25BB=P_|==PTmbM@(W+ zK#-U?VS+fd^%*f{_%LCN2p12S?iB~hmWZgx2yyD@M%>!^E)>7)UL}4-8^%DVpZBMU$e>~3 zn;og*=WR>Gj|XzZ@8I*-;Yx9Q>wDtVj&yNv+;pU8ir*@qf-O8MzTL4NeX>IQ%D&w9 zk@yMy{2OfJmqE2j;6h56t172j=rV56pg^2j*6u2Og<& zH_rpJzvqFuv*&?%lIMYWyytKx{IU~cAl;E_82+w;I2>Um(k*7Lx8f#-qw zdd~y1m*;^;>U^>1fq9JQftf#D?P1`NI(PRxFi-G2Fn97iFpu>-FyHNYU~cJo;E_7_ z@;op%_dM`Ookw^cnD6vFFi-S6Fb8-Zn6L9ZFk3G{dQQY6b)MpRV7|xm!0hdLV7|li zz}(OCz~9&HVc7b4zk^igUi+ ze#QhfNBu zv*f^O3v6zCw&6g@PW11`-+sFVn(jE5wK;3^!K__L9G(pUde)|M{^u}}vI7TBU*5B+ z^MM0pWto|64`k+SFZmd-rMURpZdd6=W176l8o39|o<{yh=XaTAi_D2|zPyeP=6Wt0Idla^UP> z#$^Y7dNZ?1>*qM=e1=3dzja1{%A#w_WY2ESD99?vO5O%ovSizm{G5{QpO;jm8A`tX z7+UX8=~R4M0^$h?%8u!wG-W^2gv#8k>AWWY&7ZW?(+th(FHJQ4^Rv&&plw-6O-^3k z=7Nl@f`lan8HblpT?N~ARh430nU)6Uy>siBgYG% zY*t3Xl3j$-lvNo^B1?S~B(GT?w=p9lQtM|KZb@sOc|=zAQ_082(X~*v;hK+>RO)nY zxV(EM%uh81$||bRv%4Y{>?&QeW<^G_PUrP;Yy~|OrKRO$E@EBZe2jmL94V8}jJg7y z&M7N1U6pEVy7}>@Kv~6B_N-Osl=X3O1tzVZ`4pG3FjRC zdmE@+r*r2w`U~^S5cgUG#du??s*ju|UDQQFT5nY-sSBvez}~!k z_Uy`ngd(`G$Yg2>odG^Rrd8bW0+0lQ&#BqJ7Zdny=&Y}4Z{CI^z&dudKHWaPHz(h+ zm5fWNEwj&S`jh#|rT}$@nrqec8Ci$iC!K|in0uW((=j;7%U^TSPnvKQ>In)OzlDW`(` zlB0@$X4>-$>}pC+&&h&yCnasF%t^P)(LJO?=k;;xx29FD&(GPb%Q&s5d45x%Of7?9 z!YP=EiaHmuWotcudt^l^ZCO@S(l>^%3vz~58K?-zM{2zYw;W`x^P0!h}E@@Nk z-uZR1ED3w_sIsQBt5S_kH$T`EfX>htdP3FUc#Y zI(k%>WEV}(#QdOi-iUs-%R7w+^8-x*+0Xe0^ER?esdIi!Svqu_GM`u-v8NYP3ilz7QRw_H%koWy+eA zlwGS*^3$JAFRq2ositgbEJw4n`!dD;)ZX@cT~_q-J}SFc9wim0@3WRmFZM~5I#;Dt zt;jF2WmrozEn;rh1lYG9I`99gUAr9T`QNK-8%bEataw=0+ zrY~Fe)&5HKXntOP%GUj5#kJX~*|nx@Q&DPlN*t#csk6p@-&cFIs9l&|%%dco%8`*< zs&c@5*|PLfpX3!A0MxlsCP`ATV#n^;UxQ;QpRA&y>}(&OB};13o!4Mj9Usion*e)q z^7D(+r%&ILw-pAQe;i65%*!h^nKCve`z)z7nUdp_(&*>u76~C>`Z6bVrRS~LxFI9K zvMp7*AKqM?zGwRMul5&3N}a3Is#jJjPSlpV78hr2DJspzQIv!o)Yu7kr%C3IngDw$ z@>ACAPoJK?gd6Alz3Dks`T5m5Hf&_47o}#Wnu=CPrStR3(qVcP#ij62RmJIF)vYW& z3$7V^al4+5%&G+YefxJLZ&(SCI!jJ?N04RdSc|p{q{ekwyGm(NKYy_YJ&T@RTM5nL z^3$j9SzDTqJv6O3uQVHwYRcf#8l3EBWMuI~bjcDZQUc>}fb`99QyVwp*u}o9D=YTw zfzDJ}>b$BnYX{C4b((5XY1Ix~>wPq@Z3=vmUQ?BlzoCLUug_0kyY>t0faEffI+xbg z))o~V-iW*`9Qs0A4Jz}}zi_Be>I|(nSdU#~;lqaul1gfdl2@(*pr2PIYbrW~?Bj9= zo((*jKWrM5m!oHutIAI&i}I2ToFlA{OFg`!2s)Rhno6tJV}GrwE5o9K!bYoDvSdra4pTB!hRy{pbY>qrFtlgy?8*E=6T#ZEah2(kMR|E1HoF^(rESx^ zGm4ToZdg}f>t(9F1U~saU6o&yZ-%fyz&{)UJ&8YdkCPR5i7JGKhr{xwFyvkvLs`OZ%sSO_I-{|&=qq=nCN$i-O`Fzbq-AVcx65H1 zQ{ykRiW?W2-#-mlUA}*F3i{c~qCBr4tGXJqtvv6y=)7@NL9)#!BWFitg?fKkuw_Y6 zQ3j_YC`{O(bhUhz`ebKp0#N5oYz>>*)c8GXwT+CJm!B4tzgL`JTW)1hUM!ur)&ZRh)`Vn$k4! z?9^JRb8;FwI~!v;EAN$ZJ1%;T?#IoqoJmkROHHAA?GoM8TUn9oP}95U_L@8&=*&r2 zflX)G*Q%d2olEn|9eFsgbnnsp{FwxMo%0H;Q}2eA(7A3+&*hd^RBQSoEJ)>Q#tNn) zUY$#Z&dHn5&!$>at`jD8**1`3e(p?yTu54$k{dSW7078<&ApX1xz1|JHC5?!&Y;dV ztxZd8wq06Ulx@n+y5A`mXPjN9nO{DG@JTKf&s+a**IhEGR^$z8H7)E+ZK`=R<6h^u&E54KXF>qC%HM*t2CW! z4|5KaU0u2(YjeS2+0RLE+neF^%k#2KrmTuw=Zu_kcbaH^_6))&yQ_58yrC#V=`1}n zbaqiwZenexy$f$<4hVa~RILin6lv)Oe=in{!E>yL$`H7)`k@ zS-GmhF5Udd83gETQyI2xr*m#S73J>UTmZ!~idpChIfvDvyLW7(x)L@k^4vshFtf7~ z>tj|=Ju1t5{~3hBnri60iU_Ez*}Oqcy@;<{SD9O1MTNP=n>QsDR9Fg~EX>W_ontE4 zrKy~3vgDv(ktusgO}*Ues|k7Lxn~dxE2`AuS=ux_b#aw-y-}H4e?_@D1sSU<3T=g; zcVeYZ=iI`RGBX#REES&2Rar}vW<8!&pDF-%d4~~3ER${Gp8`IRIWKTt*gdU{qS9G zMNOf75t_VGdRtNLj;zv(8mRqU;qEP^^kfc&?Mdr-GS51r@Lg`rW?RpKH58F+j#b5l z4OdiHrwsYAtCMTx99Fbrb6!qtZf-1=q1jmxb&5A!1xuLY&oIzLl{r`DO4fzDvoQUx zD=utWMc`MRlvI!dMKOmh!NGY}amDUjyeZ1wQPVV@=KIbx9LlYzl%A_HSFtWEEUu!? zO{@s~Hp8PA-gl_5u&^etGz(MrmMs<7chQr@H-W1;_RNE&XOjvotbZx2*<4+$JvF{Q z@Y`G=$$x>`oWt-INA5M@$qqH1pE>3%z#;Z5MzEE2;i1F2Cu_315%9~gGGlk=@J$_e zmqn(+L+*01mNU;hE1-JT#u|d!9-0sk4Pfp>Zd@hQU3fX9HJ1HS-%3H%E9HSjp_8{oIV6Tt6)-vfUD{s=q?{0aCo@E72( zz~6ws1OEV?0{#j73n)-uf6oJRAI}5xzdR4jmw6tT|I$aC!+U?PKwk?x)mwW?NzdUZ3%VCHk9Dgg!(T7({>id z0U`qbhC5o*yhOhlp$+=EMn!K#0E7bK|*Oy&~JTM1&9+*3L9++?QJTMRN zJTUkDKLXAfr}%M(U=gMj@_P;8V&Dp3Q&|tbyd8Frod7$Tk zd6?&c`9jYF^Oc?l<}RKG=9@ha%!51+%y)Snm~ZktFkj?(VD9R9U=Hy-FyG*LVD9O8 zU>@apU_Q_Dzv>=v?0I0m#`C~@wdaAkwda9H>U@jmfq9(gfw_n0 zfqAs&fw{fsf%y{819PC~fq5|QpftDKLpcxO?=OGSjUg{$G9+99)GlV8PTU`SYBw3> zeNW!W#1(sS32-Ftc=DF5FK*IK!)GX-XW?#b4DOoxiU1LZf6?;(Z5VEVs=K!!6D@&l zfq|e47m3z8qC=5F;5AMn@2-+E(Q*IrJ|AD``1@sq`+^L%1qj9I4n`I3D)NgA6ciK$f{x-^gV)_;sY!G4y za42vXa1>CV&v*#Zj84)W%Qg0S8hAzm_nZaZj~!JgyC z?7x4}fNt(&H>iKe&M3Ja&_Z{ZHlj>}^*i#?kAKI!COkJ-|CCco|HGr+`TGB&{T+4s zyY>I4e)|^iwDynddaoS+{`bXw``>2&#*n#VG0lh>ViGn$ldzYVCr(WJvgD9=mG?6f z&Jj0tzMNYa`7Jk=yG#rnIe3h3U~G8eOk<46H`Ejx9~No!9X0h{W4H+g7C!N3yHUNX zytAJNu_1dTiTD*chLT>%L&fuMBW?Z% ztXZSI(xKuBQ85v*vlDy=#zse@=BU^h6iyxfbcZd6ws^mD9T{wYxtWf^^8v9muLMj~hr$79TMt zHqjIn6Y1N1JZO9O>~$@2llqpntPHO5e)VEfeqres~A;CCvYV=&+v9WQ!!Nys} zXx~w@V?b?fIp5~z`RVDo_0wv+OpMXmO0Tkwc*^X@wq=gBRn9%3Mtr##7#ovdiVus5 zF*&DBdSm|Aw}c*twc_nXfXNgeYBY@vn;Ol{Z0e!6JN&9E z%&&j4FnhJFRyfc&4Uche&c1NuB{shNqAH8BJ344Z$A?8H8u>lVj^FfOjzARfLx@2g z@S#c3L4=y-MjL%cC>ujgLHOHZd*5pBrQ0C${t7Sd!nM-v#mKOk`ojM4FMPJ56oT|a z;fGpyaa*kwZYRb^8E5O-?)IHS^U6`SzbfmkE89_(MQhl~2VD7j-etS0yyqvd@jh+t zV<}H1eZ(*_SF&4s$G>-Y_GdcL)d7lXmO7#rtiGi5vQc2)L$U8ZL z6VfdBmWwO4c;{cs@@xO(H(IGG0?}Q_ags1SD$Ze)`rhChD~FIVCNe69-R%Fvh?^d+ zuJ(Ro0QhvO_Tnjot;vQ3B+LvBn~R~#o;%_*HY>7wjrYK96`?~i3sV6*9+xNN0bJ4*62H|&CG`n0!=q(9@=EOzECct%% zh_om5`f6s&<9oMyrI)OFFJ@1-%VxM|QE0A`rdl3uD7|(@>}>EG8l> zKEij5F(!W8y^uB0}tsN zs%AMg$Hc1Hxt$0J7&8`~Dp{qrdi26mwU{#6LC&Xocy-mWYAc;ls4;#PI@O-cob&iK zqd+!)1;}o_+^d_8>_P##C>xxXn8`50vpA&$&iNd7&5>96B^`GCOBv@utMEXx-{U&!x7zula#fRo>}0 zqYZ*OdU5)YG72eEHXoZfQ7z``Gn5=nf!m013R(WTQ1rx*D$F8?9Q4YY4No;gfL@>Rhp5FJG zTW@=-W3~6X(V%~1yjKsG^h3f-;nOj=qwVNB2Hn&BvMWI!q3CbW(ObJgr$ikFkvp$2 ztnA!zVD9y=AGoq)L4HpQ1NtI;Hw=h{TZzD6k5Owp`=LJNS@Tt|W>k4EiDol=HT_>Y zE}gWtgvU4Pj462Hm=S|vO!RE6+s%4?qm8_mj=a4cIo1d|x}U^1orm>)WaQfc^Oh9c zKjntrI=b^Tx`D>%Xw3jyi-AGG!P0==_Pu+@qN?R1p8JpavF~sEV{{)KZ96?JPU)>g zA3gE?+KJs44|&XI_XEDqz23gBg$Mzr6iasx8Wj*QJ1pL)2Atkn5B)IX+#Z)bJ?y0y zR$bSx--xV!x;m}RG$1-Qe1;})TQMMb)WG4PvYD3r=Z^Ewy>h$v{QYvYmfWZ#@2J(z z1>IQV91~h9aMZw{yT(e=%xSNz%}F~y;L*YF{gVA`vyRwDt9Jw)jx!%K&%sgl*HK?&rM5XtI z%(?Jd^mxvM@pa(CUqI3Pg>}$NKH8eYDI0$WHn-{6*mPoxfK9+0qf;g!>6?7t`yT!~ z1I-HmeY=iHXKTZdN$7N=(d3lJwGM*Zq2qzKGs=lceYjqau2-pG2VQsTcwy(_gqJJL z=D)k*p--OM>b-O%G`oJkm!FQ)#a2%Gm4masYSp4@Tx!I|K{+dDef;9vmlgOs{qtZuR<$K-KwZ{W=T-s`~1VzTe;Plh`EeYrTKE;?4 zxc0!(pRc;-SkM3+*|{27Kw?5%RJhTB^zh)zuD{6>df4APNf2dV4D!O^i(8!(9p4$KDWm|>CQjF~H& zI46G59S@&VFetZIL}1dIzdjqRV`Xz9Bg3Mio%Ld>pLvhr0?hN@uuVU`ZHR@MYhbJY z3XDy}x?JuE9p-xf@e99Pcf88`y$&ER_4XR7tK3SCg?%tiLyT&fB$o=~u|OMtm)Kmm zI`sC5wcd-Hiz%r^H!dIMj2w%dP-7%dF6`&VIql!sHm2j4pyyw%Xx1ipNblh~;tRAk z1o4EZc&qodTgE=|P5k8QXFvCUWZCaY#_B_#1?w1Cg|{#mlZZ`&9d$rL%bEeJs)j$` z=ERJ4DX-;>&{13G91AtJ9n&4iYjTsSPZm~rr_7fPpEFWNZe3uEiZ;sCv}JjK#t$Ak zayU+T=S)V^3Du7%(W zTYHVsRc)Ot&~)@YFSf8-rJVQd&{q!5_+;Xs%@>T*@vu%`b$HmRbz=1$9YWhxc`u$#yV$&LyoCh<94u&_ z0S9ei2?=u@y2jvD@My_3)!u8?gSPt$uL(L@Ym-PXpt@y3lxaH73PO!D!(vQP;ozqC z)N4mKTXQ+q@f$KJ$^IIiUvf&Pd0D4XZBG}3vqcVa2OeHQ)5czN*>L}-yLCDzWBk)g zCR%u~lk6ImU2I!n*tN*GFFHK3Z9@N{PhN6t;Pb6U-+H%>hrUsV8|TKw;s9`9BKCCl z{qp%2{k^+(8a5(*@1?JOl$f3PZygKkNEs9sZp0CiGcRd7L&iS9aDzNUK0NA+sbh7= zz-%0azF*(+@}ygEu*zB5P|FOk-?(8LR&ybFXu`6|lvCZUt9_FMj z{O~~RFx$Cjb9rNiIrGAsA1>CNh_;aTWSH}4=orIV9IQQ4O9|3HXen>RFej<)$6a&B z*^VGvp+lK2M1aF#+&M5erO%W_2VP3Mxyt+fQRG(oRckBF;mbb1aA` z-{QmT|Gcrv`y(DWU`5vfo<;~7Vy|S z$hp)Phl`q~Eo&~_c;P9W_Z&gauUTH+)|~79h}pk>%y#e0S4H#Hi(Z`VBh>aAzaTs3 z$NT4W*x{X-t8&i65Kyg?^!t={NB`R5y;7dYe?G30;9ivyyqrBD;>g9--p}*=tz}oQ z^96UI%z42f=K{g~C3F1O49)7h4Li2E)aR?|7g}X22>)|T%~70-e5%exIt#U30L_0c zuc`PLIiIMUi-g(=z^dNpQT+Wv+j{Y0p;nCeHP*um#vklj?Y*)M>e+7ag4;#yw1o2> zyuWzH*Hzvt3RKRe@*Wm*ijEJSwezBC@0{O}^Rze4$+%kJN}=ZCZvQzMTIK!Luax@h z^Dnnb?O)Q)xN}04cdB$EUwwRqH7E7Zm&bDPGU4SPL3492FJEg;TuC(UN z8@M?!G_cBh-P5e+)0{5W9P!qrALvKPRf4l2Eu`b%F4=ePvCVT`1!p?u_-_iHIqS_T z@1^gPTlxpx1ScZqycm1x<-dl#Gvdil9{Rr1cgBRPt@WJflko8M3(y7upfO(Qbq#(^ zQbED?yUIsz#n@hkoUVTux`RgLbPP832-W3WYt5;-=DS&as%^4eXU#ddb!nUUhy}w^ z?~EPM|3t>|>t%0~-;C#8zCLNUY0ztvgID_w$_wZrls8j#{quPiTA;!A|BiM3kG=ne zRocK&SNZ%?zPx`@ur6@dS$O~3aQzNK>Dxc>RW;MG@Cm0% zd9!sUq@`*`-1d#dugW@#>9|s0QyGN8OBg!-Rb(3Ej}?i3v1vr zu@O+IaW}7A)nW_gz&Z5GUrkr3kMzh|(RuTNVXGI9czW8NKllPo_9{0e|l)TMr`=wu61Uc}0hbNWwb71p^L-QEu&Ktwp?*(gICL+Q! z*7~`O1sC>sluI5wv#+1W2uu5;AV901@rJcf)*dcf$950f#)a)sSAqa&9Vtz-zINno zL9TM_rMJ<#(}H|q;EKNCw(C+4wd+=^PiA-8i!XQ_Wmb33>uaqCLFMa`(zUeCTF6!# zZIqVps&Dz~Rr@7qH7bnqroIvGO@U@l4Ea)%zo8u?t(G_N>Gfj3f%p53PAJHKsAZfu z-oRJIHU6};y1I;qZAW7a^Vu@I3ER&YOeJRI@5wqTSNtuW^ZY~1} ztnL0z9}YfZUf{>wV2G4_3@kTB43hOkLV6*!MquS#*SAu4w&qxrWsA-fL1GSSRjsAT z^emq1_PQ%tO+(z^*40g!>v~9Az2wH(O+Xr(!~-BgU2CsPWvi2Iq{+>;tgCY<^iw@R z+Y85DQhifLwUz7q=xswBGnkJ>s9o<{1Akx+sn>*P!=^3 zGz}<0UFu*tCfKv_vi)uT&Ay+{1Dm=l7uVFb9&qew_MEa}dX9ML&b7Ik3)$ytd98XJ zMWt^8%{-3w0n!E|b(BHh5-&Kl;re+wZ-)VOB+~az z04$pRgxf^!CpGJKl#R2kHad?YUe_Ak<*KA~9BF}dq_nqgcjeUk-Z=2&`*HjJaG$S| z)X^)~T?;7xLEoQ@uBM-OCcrz^{lYeiByD`VsrRN}!({M;sclNH2lafYTiE7f+9XWyU zU|dblgbbP_;bVz-=QQbqo$bT) zE!P(^*45g5zRugRu62aSvD(my5W)~~e3T>{=LD@s>q=tVW72yBNbeO^ z$GYfK3FOnpsU)uXC`t6|9_N;>anQz|nv3d=J^iYPC$V)cy*^G#8v0a%)-HAXTtAno zR<@rt9LFEMD}fxp1lF~5cQ`8P=w}IQY(GGMOCXm$z`FYZS6q}7 z^t%L2in`-Xqi2sLVu$J48ujZedSaN6-MHQ9F=s5WCYOKv97%iKrcqXv)Xt%TGuSaLh^VV@wz)B=RA}I+BuvifwR>I zXFGZT$C~G$Hmz;1MWhR!?N?JFklseCYUJ)hqhgQLT`M~FvFaCB{>QB27Y{Y+&XW!E zQI%cSoirVN$2Z0 zQql{61Ep`|sj_}otzVOL!>-yFCDmD*nveOrGJMOWKXcP)*hhp*fr^H6&Ak`_@u(vX zae88PM#gf@fp5G{Smmdu;hCf(jkXS99W8*=;4Z9V)hjhuCx$2lIFZlN6P=dURuPm% z!HIkyL>yN&0==ccR+5**cs{|?1-?2oK_c5(M}6fcqMSD8>&sEjAe{7bZI*~n6Szi5 zuG+lbaB1?^W^jG!%aN{nWfbI%g2ZfRju@`yIq&FaXu~M z$^MC0D+c;3!ukG8+C5e{&W{&?LSIanf_Xr(c>T+aKTm(un_A6=wf%*AnsCh4H%>I0(ncuDs^0=s zZK&1Coe9CcS{v4kBZlYne7{4Fz}BM|B)C@xeQo`kx%M^O;Sk)bsUe#SM2qpa4QIj| z(x-CID&IIGmxYBpW;>riULT@%4QZPZTBwYLev9Kacj zGXm!?&iZQC$GRSpHMv_?Lvzp|)T4TcJ1tcsSCI6o_C3ZO7{NUqxwfjWExAtOTSm3+ z!(x-H0q${ECl{_;Q1fo|K1vE9=Boyv+sl%D<|T9$L>7anO;jScDKSR+?5 zk2b0Xu663uiT5_+;ZfDzaV+{~jvTwR4BlAbj)ZnWD_qd1nj7-8iaS@QTzQAPY|+OW zRck}ON!uYf>h^e}YUzy9v}Uy%@|7!T?qnj6{)B}SSDOU20?^;(43PT<-au75jc}~? z<3Ouczd}5z2(2q$TFDr2pN4)=GlcDSk~%H1zq`b~ddh(ZA4iY&lJ9xiBi@XQRs4_$ z!11Chw{&5`fSaoRQ7`q{e>J{Tz#kqk6ui|DIqCxI!mH*a9>7;2v@5XWGkUb;i#}cI zuw-Q*#L)-~al8{ok8YuMN#~1Zm#HNAt)CF3EZqbb^E}i@5a*!4UCLLf{cf1m=eI^y zoXXX~v z)zou2i?l62xOj9HJ$vHk=O4c;#SW z03zy~Q9IYr`dPcBogiLC!6{lnt(bCh3F1X$#tlbp{HzFQ&^OVgOlr{VGnEeIs=E5+q-y;S3-;d#*KVda7-|89UECMbAIyw^);mUkHBGC(QaCgwL6wi_FU4EqS!x`856&($i z#byVflTcQQOJ3pwd@kfF#CE&B2j)7Co+ykRU+nzVTFkLlI>!2RF*Mg2sU^Pj2|&#Z zR`(3L??ksCe=W?(Y#Cy3=_FgRl{?6i7l^7NCHyY7YqM z-fiW0bf7lNbY}Ex8n73=ff|2^fazG@K+It&b=WsfD~M+{GLJ+;aX`TuxDZ#od03=m zy~k1>_BxMUZ9B>c$6!~bePn4`^aPatmwXb^Fl?pO!3VYEVVC+T^~pNklv`g~&zbe| zvV^6$oOtL`ctMrZfdYd#X`n`TyHRqY7dyGPg^~gKAJ@w{_h?%;rnUtxV#EUN5Zd=j z1m+t0WF@aE#!|0YC>W48snoSyErUJ0SbIRI@n$L4{mr5Fx{itxYP?D1OrCR5<_#T< zvO(&qE}d#w^i6MS4+xb+S~*wTX?<_$sAvZR(2|^x%+SRKw38*aN7;`&7+_g$G;MGm zqPez5IckQ>BLp5^+MpAn@7NwK9CIH}=>z>(CvAybJl+sp;U&BiuN=RcFL864LGir+a6_GjFJ|u z4p?njmmvC{?NPQaU%}J2MC;P&1y<;usim!)Yc07TT&bg@miBbcwB$kQDjgMl7FUz{ z{<#8aDY`PWepSY`G`=YFzK(_?-$vt1k}FYSwT^-o9SXaPmYW8y+BE2bNWS)fkoLmE zW7>72EEca-`_N5z((DBF@p)7o(%3(()D zF=7GwMSV%vl3joqcjYzaq7XR=+7a@jgM+9wZ|^kc9G(sFcVI8wPWx6M+_yaBja z*BA6U_Sf0e){uX;gkOf@^SB9|`kMOD8R}7a|4P1451-WqsUu#b8e3Zj0Mi0vBtXRkz3|^}#maQJmTgF^#hKL0AQB z%>)2PjyfbJR|N*FTlykeZFy$U0jNJ1SEHsbWpjC@q6Ve4hNcDb%cO~x7dpO9>T4lx zM5(Pn4cN=1GFcJ~Xe%B()@lV1a($v{=<86rmHfi|HeG2YhiVV4v=*vj25{_;=yn|y zdsbhEJ-tTPTHJ!@4jmoWRd=JOB4KrRqZcAAc?R?(4>or?lnp}b(+>?s**I(2w&FI# zcj?Nqzea*@7<@svwOl)SVP`jp%`U+2^Rzl<&(B(SLM7=+Z~ARQIGPI zD~nM1GS8lt#{nF7JZ_(9Der|BR(B%4*YeDO^PDq!tsQAQeSq5WqwTeyV&Vm_?1$(+ z^(a5Nj&R0P?~!eX+KSK%w(pPVe)T9nIpUnHVU1Xk3vobKlJ+tL`kK(r)6p}U^uE^h zs{uZ207@Nni;5$UV8B=!i13hnQXj7C;y5H2(4&C}5A(qSuBrN2N*Hh*Bna^%8VN$z zR2+i@19mop5PmG5)Q4*-j!J?7Kk+gc;ZgadK3r4XY$;{H&x{R0{1dmRI64UiT+SGZ z@KgDuK3vzuu}LuCwaD>O?llrKSj-Hrcz|YhKBm7)G zsgL^TY!=cPV^xSR+@!PF$VISib)8ist*<_tm0XO|93j5ah!ECCXS0(_u)Y-HYd7g^ zmU5{W4VvR_(%EbUzY-6cZ``D_SDpX zH|cElawS#+LVWKgoy}gl$ivki+@!PF%T@9s*N<+}+3ck&<`f}Lx=ClVmu{E~h4{%$ zI-9**EzgO6c9YI#FW2yoK8Ro3q_f#eckv(4{OTs1&0ek*_kiX%pus}xe7@mv@MM(u zy}^R!?txR&&`eCjWbCr)UC&!#()*gG?YF^N5l1E09M+3 z&92taGp+p8m9o~l()57#XHLBMrp|7-3({aZBK(Sl2AqfVCqvGuqEM=>j*7L}pGY{T zyBFo!>FC%Fwq3Pzni(k5UPr^a?DvGu>7r4tgN}}S0LLAhb4s==ztXD#M}huJvs${k z4Ax0kvNf`_7mmJYNA>#KRz4O{eqBlfu7_>9XsLLgfoQ0%9D8MmT#s`tHySp;y&-qg zb0v|N{EWxv)u7S{RZDRf$hCQQv@~DFtJm3@4H(2&Ljve-)QAvwkM4xr-S|7u>Cf<3 zr+Q}h&tpSoufL!(E++T*kY;>eB>Pr>?#=6nsud}dI=APKdT78MEU!+o^=-hOBC_BG zwq%+11QMCv45#L76NZj|2?X-abNu@fpqc0RCp~UQn&Z#VGp%67_XG4l_y<2eZ|}zU zF8}Bf+fV&L(|O(wKcqK$^tp2F&Gzz9vO#~InVyFyw!k#BAy02j^6D{D=xrmh`!HeU zs@^8>tAw#49Z&RTv_+oaY8L`Bk;Yb5&-xs`+@V#nb%?+H{<{n*4ac=>IN_)Aw9v6+ zz3J?zw`a`_zrE{8KdHFPprB6`~%1RLSFB%V$8!o`3(p(8j9L>^p}FH z{6K~K-}L|2?j5r)e&%&c`Vvd}Gyar;jY+i`hf!F6QS2ms*lZgRNfItWW;6<^se;G! z{HE?y81`J5HcoaA-3CoP0x#cpwBg%LBu$5oNN;3Pbx0( z-Aft`VDXn?;ENkF-! zXT$5~@p+&i>cROl8Ft0LNDK-(;mPQm$v8H;5zcG?wyT5i6r}sdvIcg=r9iG6IwD+v z&AL}xg^;cp()|r=bFJOkFLe{h<|5_*8SmYzU(a5>`2S|Pc8?N;^9<kR?6d z|6&y2@nxLw#o@U+&eZ-gvk4A`ue#Jy#%A9=K|>wrj`TWMWY0%(6X1kjC%@AD>ldW? z?H|-^PTQ`PZtK8~>!{qDX0x5G9Fvuc8#iBReW>v=e1Ldu3&>Y{}kR zWfzHTva+&eZ~f1?pZ9t1eO@n(@A&ikx^>?De9qnH?sL!Q+(kK6RgUs5sCY{ zM8n!nu4Kl~WF#o+2*jidbkPGqK?jVP<3x{IOfPQ;Qiut#oZ{4>KudBNb2@})8)n2| zY>6u*Nya@cM-eAcE=TlvTn1w^UYXD4%D~gj4FWO#m~b-jl!%|6He~Ofk`*|ivkOg% zGi&Ln0{=0&T4|_q(o2JkXy{s&$)-4DLG%thGtU?X32zEE5*|-uq`P!OAbnz)#OK5E z5lu-VLvJW9UOT&lJ6#&2x`_BWXxer6#Oqhd_A&H z!;!6?sE`3jogn-P#vi=Rn*vR(@YNjt48)&7_%j%P!to~peEzMadmi%aV7~AV)bea}i7>Fg1zh zB8(Xf4q60ojm`kBs~I4^L_g?W>RjXw6hc71IBjrnbAW4G*scmOR5UjT-WXHF;~<9z zJ;3NI1S=DOQom~4_H0g$14#x4BlG}!5FuEZ0L0jQnL!2=>Gik}Try!j4nSq#=Hwdy zjF$AV5Lm>(t^`V82+!KzW|F0`+$m7HEQB{F5Y&YYI~2;A;+8TnLzR>!XekiXBkX7x zY3mdIGPJi!MPop?#R7pL;l`2@iGBnEBiJU;Mj$YTwE%4tu%0=M;kT4*!+g@^=t2HY zH@G$cZVfeGO^0OFfp__*~(1Q@X-_gT}`cs~nAQqYa_xJP0B~QBO}#Hbt@DNI=q^ z0q4j7ld{MrUB5&wWGTwwc>h}68CxP4G8oa_nMOcUmt7{0hmEmx>)>#pePA1%4?$!o z3K&_6b^^*Vz@n}g&!mhtQlE@mvbm6@=r7ww&*mhF21uq{<)A2dMT#x}Tb80Ov22Rc zu?m4*!{=Sk{phG8L=Hs-Hf)M!X7G60 zp%y&PI*%NKgC9A*+;P(}%3&(ZN*X-^pwjbi)!ecLFL`wF(vI4W}(nd?`S zq^K~&noZF=xBK&c)NR1qzRk+<+0%i%`4@b64PG5|ME1vX!9E#kk`+GSe%D@)V;N0V zL!b>~O3VqednR!kT?s*C=6X0bVN+BTE2KkFIEGqV$C9xW-*Fd;wzTW+`fttkjj;T~0mUWeKf1yP0w6`#f-OVQSdRv5iefLWJt~(_6KAG^yMAP5!c{(s zf=FmdKmE6R{koDAO&UF#O;KFoe@?a}a5~$aci7v7*CxV<^lS*ut7})pqUwK}qNyb* zDojXVQ}o95-aJ3c)&#I!PVzVAx$b!jD}FPw%BN>Xx_yd%Vdmogx5iOYaP0N#OHwr0 z$A?W(q~d|?jR`>eb9|oc;ixp#7ROHl_4u=7+US3fKPsN<(@Ihl_gfc}oiIvOJnI3LpC;PKVMn<#wDp|aLpsF_ zY~OR3cSQ5Z;EoYpL+qM$?K#w;s`^#y;QOlgciumjZ`15)#E3?C+7AbMZV(EEu23BD zs|uVLak$0s$xx7rGCV5bV=#H-a@C80-NB*pTqOl{MXr**a%~lLRed!MIw_(PL`g+q zG*mNuj3$p}_!vVTP4O|7Jl4j@iC>O7m#e^q_#kOGktD9V=t_c1T$Q;hO;ovR#Mcnw zs}A}iF@rC41;`?t-4YkQVvzCS;3Ik=DRRuYstT%#s!FQLsw%3gs_0o;1&%bUC{fKc z>TtMKmAKq$N_0*jEiR^|PEs36;?&1Dx%h}g0_k!L3<;A`4%YSxGE`7Ito>|8eGT>7c#?DP^b<*G=bh)O3Df< z3fc;)3N;jJDyS*cQoyX~QZ-2m5W6l_i+QX@DKWXMMk(VXI_nZ=bD*_?qPY?YQCox} zFLk6}^dv9UD0Mmt&~Cy!Xh`C(N*)!tkejM>0Z9QAp#WGmMI}XLMHNL=MKwirMa+hQ ziVBoc2|ekWbIie;Ih0c!%Bc?Jbe41}Ly}`cW=#gyKvD`_%8Uduhx%2(e3&D(@u4B}(1>}kpdV_8>dS)LSfMf9 zJZp&RO_2*_sX-|)`KSTSAKQfpPm>>B6o@xc1sw9IA_<`?2?2Wsl2b0}Sn5j3%I4UR zTvU|d&{rLh%8HZ{pg=^61TyDB?*Xk~_<|24aeN>N%g&SEo#K4T7zR0;Z=lA@9l7n`Q4lA4k_j2LS~9+Whc z#U7ND%~i}*)z#E#4_u6)7*4C{(m^}xhmUszFnkPHQ({{!LPmU z9$Nl@`?}%Do83pfw^aQQP(|f&S{wV2uA`#)_ujj|)V zj#$)SuG$wKwMFadnKMSQqn!q2U73FDWu3c`HL}b{$9;5o^R{ZE22&1c`0T3ty=^D& zZLZ68ly&9=`=s_cvg-NQ>LxDgFWcu899PjOoc?Y4iK$yIMD+W9qR>$1?vdFiFW%xU z(G#BZKhph9%0!>#K^fJYu7st8kM|qB=DAO_uz^v}db%|h7bnHs>nJEL4ya*k7Boeu ze0!hGy&n5U+noOLIf<8W+su7%C!Ut=!K3#lT{l`^*zWB9Z+Sm#gAS{0J2YO^+Bw8_qfQ`F~Jjo}Mh-^m>EX5WLC z+gI%!tG73`(b%+aixo|icMJ-NrWz;qGh#|W0#QaQ{NnRZFuL#n|E&U?PoQ} zI<3F#!9?GQk<*@Er;PP{dK}4qpFT{Oe#!mfqq>uir(NB2`(a{iQjg^;a~}rkwn?56 z*)(rgr)PT?oqoW{+5Yart~x{L+xdU&6MU9G?C7 zUAumQ<@tTgl1f&INkeC3^u z-q>7eT+8q0>Yrkgb*8mR#;gFAp3gc|z`40!@R-10M{q8aMjkPm1 zVyE|_HsqLZcT8UHbm++ArY|yg)YuVGJ3Vf7j>^ThEsLFWR%dkiGQ4j~)9|)UV;1Fg zOmZqxbvq%{>pe$p!paf8b=$8S)4%Tg=~=Ui7r0N3HJ@$v_=B;h&(10iqdT@35Sn0r zWy5>pL>0@`zD*Y|x#6h2z1yiRJ0`z)-_~O4^f~<6bsp~P6V#8}IPYmx_eY}>_cd$W z+hszG?Lfgf59`_IIPo`QY+lcJ>(a;Hf9*Ys;Joeo{r#&q^S9=xE$^~;Rmb~r4--6< z@7>Wqs$0GBr>yPXYid3?dVJE^aSNh#ckCbEW3T?5$-~|>^;-OP_|O||6qK)5O&`1Z z{@AM4YwxYu_~p<5fltzwFN(bm`rK9OcHzRY-Y47Cwj!__A%~0BynLaf!*_Gh1XxU9yB*9d0|X`jRGaDKK?uTy&Qyh zYL9bzm2+Rk?b(ZE@0_}ZOucxidzaVG^XDbGUXSSHYnoKmx8JeBljc7gcluP#?T21J zr#!zcHyh=2;8UB#MZ3pO3Lm1Wd45*n+$D)4?RTZNPI|3%HngAquDVShxBPr`j(cu6 z$9zch%+OVS2506fKOMByGwm3hl($ZrWTTmuxIZ*YAFW0b?e*JT)*OB9lICK&#$T5 zHP^jhvv%>4Pj1G`l#T2hb=<0IUDBA=OSeU%q?SuJ#1t1^w7#qx=8`qs$?RU}sdx3p z9~t7;`&IMPI?Jzr8Jz05cg^se=caotgq&M9@5MN8Zm!-z3L&lDB8n~+e!fKqZ4ZB@aymQ2( zp~jN+qu+F{_4P{b6(zU#V`_Xo)}8P9_|4h1o2uNlvh<6aoz^z@;qG0Q8^-nadKq)S z&zbnh6VW!YW;$8M0|uuUUp;cQbMos|quqrcJF48?bjBkjWtZu}nfs6G4@~+nfAxL6 z&GnZm4&J|ddhWt;bKB3@oBp^r_twxC+p<27sq$#nTMOa71}BZ?*Lsw;+@XoUb?wZB z^%89Dtvip3m|5q;)(?@dF64Ex?|E|L64%_hh2DkSfsHb2b^YQTcu8&WHs0B1>F<{} zGV`1ea;;!xY^afTW)rWoH`Nxb(B`$c)=-euw#~;?*<&`gu6DK8*JzvFFXv9LVSjMu z_{)x5+ojt$?&pk07Wa!^C=9-5*VS&(R`JLjrrJsPm| z`J9zi#r;~x}ny`qt!y&``4S!-<+MgcAaG_H#<9}zN%FRJW}}5<8+HB z{oZ%o!lzmvH`hPCC?F^{kpMsIR*UNEU+e1m2S2JD)7k8`I%;TL0KuE6EYVq+g5n=rFPd^y(Sh;w>BH} zu5I>Yg~TdV)sj6P+OOOe8#T+aXUoVD$69>ouhsD4mTy;&PFC4>bmNoG!eu>XE$Y8z z)GWKha~@Bj0*Ak-dDFw}gl7NE2NiFn4zH^ck+wKxqor%Z!JqV+zFE=n){HHyddz&; z;cH~##^#L#&M*CzYb^_%b7|s@>S0%wH5q)2d)7(#^}v@aUsdg}|%;Mud$ zSq&CH+8;OgLE-q@$$AiU4n3#qq1)KkpM{_&3A zyjP#$8ucH4Z@gj-HLA_^?k7#E`nzpDwZ=L4qguBMHT#7gjypeKWsB3(qXS!ycr^Or z*vJ(cM_c?TzH#f#2$eeXj_(b=_i5?a;!hz~jh=-pt$HQ>^`)Kxiu+tGu0-YaZxxYn zf7VNlTi;Iqc$b&hE~Q$NqSvYZg)>tZ8qXZyn`*1F!qfF`?wal|lZNl=6g+uJ{ISH7 zpL~)}&o(jiPv5ex{VD!~o{c8I?2t1vA>~+uuv2|qkNBOqXcsfG@uX{Q`kQQI)%QEfg*o|LpRaerwx&6CI%i<9Fz;acR-Ki)cE>H<++sfT-|noidyLVo zaW@(dc<`!VUZTJEoRg7F4zIr)h`BmuYx?&1C52y~m>=0$ZB#*l{m0&C3L9v>DTwZ9(RFx$qF?cVUY|eu zzbq=M-r>gJ+Rd#O3^ltq2<02y^n{gcTc68 z`U~D@&xqWy!!|v5_m??c+8aN+YWE}MZp-ae6OMna{_ z@XC++8^^`H`eIrmuTR3j-PMY|&5Lo@>r?HR%SlC>l)dpYuUDDJ-C!CSF>KrPZ@Lj` zqvl7;nAIbk_u z@y^Z<=6Zz=E?#tQrpbwitCh<8GafPjJlmr17!IRP$h;h{n9(^`hD@tMV-1@civ{;mmJ=SNzTLEP8s9g@8H zOA|83PoDI>Lqvgk1b>@;&DuX6=9vdt9^TsW+>40mQI_L4-z_-PmMxh)?cOEM4Evxj z>ldVK3TilMLEW3)?~Suxb*g)g7x&(4y_TEC0k=Me(>ghA^&Pb|%sj94Ebq5PMby&a<%T?5=d}T}#jG_m{iR z=rru*hI6TpI&4*l(){w)^-fA({p!yOHmtN56kUDShk_B=e$V5l)qP{Su*!(E+qHW1 zHwtWLQ2nXufwdNQY>I>CJmn_d%m1?Ba&*Imn)$JUxbv}>S2t9B*&?&+hN&rnY2El; zRel64ji1u3)uKa-67QQjo=%;2wO>=;{H*+(n9&J)+z!n2w;A%``oaXShOPTf-m}gi zzURa=L#>@5m!^7LzS?Hgc&*OO$FEl3e00Hb@9$%}D(AhPHS^KxE0+(sZwyJUzDQ%k zn7l>LW^ZWiw#=#S$>XM{;_n}d<_}9azqnrCDbxC;)lFF2^xg1;^gTVI`c{oI++sF! z#Oxj2qs@n3xSl^(_p5%!>poNUx9RA3-mcqn`>KWosRNyTl5ghKh(FsX&hb$QzhPKV z@0JGMZ%5oH$~E^7aD1$DXOrHWRpv!=t0d3p=a=&A=GqgjvU~2oFt68SPP<`lb@FpK znaRfOGW>Evj#V+Y=SHcin3~-->NWb(_ErvWjd-uRb+%|8^Y+O4ffGZIWKHU*->vI# z)1=@z*++urI_ZoI(fJyXQ8@_ybs5J60$92@d{ocrSf6~d} zBip)mH+@*#s^12usm2?EZA~pNY*ZcG&M^HLevR>KNqN)!X!1EZuYxilSQc`2xdCjM zvr8eL&*r&1sP}~U8AJ2h>2pi@Ja_Pk{{LI&LgG`5jZk!1ikg|RDT>r{RPX7Cz|a+K znyR#N0BQVx`+R*9kV1x{1A>D}QdH6=h0y-Q02D@<5F!^MCMurBp`Q}6J+8mQ*D@%5 zMVHV}Tp}2pWMx;!%Y54*ZlhaDQZ%VUV>U&%xcT#Lnmh7TQYSfj)$YN=(_u^u<&~I3 z*(bs@#Y^NumZGTI^OyYi?XOUDTS~Teptgin0GQ0N%xhp(fg5Bs_qWD5+(+t=L`a zqgV{|OO6Yb_~wfc0NdzJc#@%LaIhSi29>qWBC3h!Ez&+$wCDgfTPl>Ma5?6#=fCI| zN-&2opBWQEVa1nw?%Gw7qC%kzMcM6BLQ#1iZHsbHzFz<07o})rdOf+;1l#EDk`yh` zMkQmXh+ZNK+h%dwY{y-5y;v*Ra<5o_)vxG47CQ;3YA99QGTSKei;9#`OB<>ksk`9$ z$1e^$cyUAK%-z*6D`Vl8kK|n{QmMzNwq4XW&*0Td&zQ8Ax9MKdMcy*K4*k6?U-Dds zGyQK@`L5)?p4 zT1S0&^(LAIE2q|pZXX9B7Mb=DJ3!{k`(y7L^6qTY=N+?GS``+4Rl+55x#)*6 zFcHpyQ>cKuKlZqX&V!`vv~I6Y$*_t{D#K)dGIa z)ylQe|D`bu%R=~u>0rxHG?0>MqjP3ik+BFV8`WzxP!c%^xEiCsDyj6k*7AR`*JqTZ zXh5J0MfY4!a~v{p3sD|=Dy}UrusxrAJ9ajwChW;{Nn6DlVhqZe|Gx7qlluy;!bEM< zg31IkAlDu_A_-5-nyYRaj6@chyF7uQAyKf0Yv5ymm1)l)EXf`rW*!{^)a3Jq*KEqW zajh5c9el@A^fH1cjb~y!p}(7l%qn7iHix&crYc!A z6s5ZI*7x)vd*tF`FJ4e^KQaeOC0;uHWjT_+DlZ2LlL-_ylqyttDcXq2f>8AUP+yxO z*mzMbsx`*Sf;pZT4(f4JhQ}VDYn4`y4@xrO9X+7UK^<=x8gJY21SMUJBl!jbb-zg* zdy5?r$3dNN630GbN0jg7psqN^v4>Q&m=_M}kdrt{q9vI^-EtB~Nwlb}jDtGoFlX+d z7;u<4MKyFnO?0$V0;p_+7g9#-5FFG`M>{2gIzh&Xj2x)7j)_nb3OgbPb=fgal2F*- zIjG@|NlW5X4V_T$9j^V|K>-~+l1x-b2h@c}JG2lxkPl2yKOXI1Ep|Yq(Hzv7hYlU3 zqQc>XgL?F62T4?@+Lwd6^=Jo4R5j57_3dFU@D?<@A0YsKqoBPEhN^J_N}}#@N!_D5 zRSs(BD=Ubk>QG%f2X*#`^= zNqsYQID#ij_bVd5SboN=Q+G8T!tKMO?SUE0yCf_w`Yp(HbnN=}VdyNfnRI<*Xjc<1 zf1wTL5#ylC@nbpBKbx)trP?Ij7H*gHE2;}r2*Vh<{oWDz#rcIX zbo_~;dMg?KUGT`xOJb?<7l`~~`v@|!SDc~Phc_VpSX`r1LwU^m4TqfyEds}u$lE=s zbiPe`mw$CkYzAY4lgQ9zz7KCqo^+PgsO=aSlfl@GPo}Rtkn&r9RbIX0xp5gyV-HxG z+4`2CI+d^SSH3SJ#M%yCUka2 zS^0p&88My9^7~5US03$XdW4oryCP9od9;%aEBnrriD}!fOXNe+Hp(i)n-ckugr(D! zyq6W%+n>ZG8&>vRwoLDS5?5*YDy==d7sVyr9^QSwH0+64+3n%OPx2vK2IjrAe0-FX zj|1)++{Tk8mf0>p{Ujfy(Sx3k;!5=S&r)&0BQd4XgDDf8hU{2Vi(FTS5#U@RJ=c>+ z;YT!n@|gFZ*~}UR{oGBRN4(_w`cylC?(9Cbmsj;ng1c>jw=LNQ=UvS zVdQ0=aigjJSv1=ZES36n2{SMnneG^dnB-c>MorF6SvR02T@3S(dlhqK~R zq5b-kMxGnuzr9v?+r$c8(1W}##e|~mqMvpJP6KM=^@mntt!A`e`qsC{hrM|ZF6xW!ROyd-sMh3cpE*b8YlJ;hBKPz=b>kj{%rrh`Mo|uV;Xr%rcNE2rSDq zb^RSgQlT6q_Y`F)RiqMS8DE-6Dqg@@7Da(NKrsgRkRAh4NwOdDUK7U>x%f>eilq|* zRF0x}HK{B`BjpHgY~-DT10{MVJ8^;%@6p2`UTMm);YfohF8VzwiU%^47u?Vw*S5+~ zw0&%QcCWvF?V1BB#zvbF)240uz@FVa$r~KoE&5H{XeB91rUx8H@j_b}iV9_~ZJ>{h z8MREBBS~6BheRgz^LtYi+eqcdQ5#AdD2fKDiqP}jEDgH7GD zr%yY+E{ft6K6>Q1Xkl&Mi)Vv)D_0ouav>iK&@s}#v|x#xl@zW%qz~SMpk&272GoP?@!5G_<*ZeI&y5;Wt*B?%)y@=`paE+F@?aZ ztq|Dp%cdyg3Bd!N*f)hgAJMNn+|^58mmLc@Z`g?pgFOgdO^w&d`%sj}FU_xLc$r&DGl94RCWTxoUvJ(vYIylFh@ji+@a9=b;fnF{Di5a^T3% zTOVu>PnXEZn82_Is10ZdaY5nEVG)6>qJZcWMvJDxL5D#jqD_T^CJ=GYN~PKd>a>!n z1|XA_m zP8bE#G)Z)FelRO1DNYS+5tz4bMA$S8mfnU`d-x53g*e{1jeeNm7$*X)-IW$kW8CD~ z>rZxUHN|NKPu9jbuvXBPbF##EC$8e3l7|C~zfz(Rhm`vuiLO%7NP;u5ml6$wW^Beg z+m>8^&G-`)ig{&wFYQf|D<)jIXd?+G8?j6@%q!!K{Vpj4z$EIjip`6 zT%@jfjf)}IHt+J z-?Jt8;U7lyIR)!a@ZBALxZH4|tciDm8 z@OkSwKCLDG(T{v0;lPXr;}QrQDI~|gnW7%$AWE@<3~&3JzZVD?mfHV`IMx%Z{uryb|~1paP*gGj%U`nyc)ZM zcBHl1`L_DT#@Po84@EuVn#_;>*hIK^-iQ3mQ8)T5SoVDPE(;yrd8LQTtf!{m?wQ|g zXnNYrs(otU|_FskW=4;N|&ZdkeP{Eo9-cCLK>amT!Q z?Pv58BxV?VnfFEOMY8gSg;VvPwQO>;mC2)}M{m5f*}gl^$>qr?on7<#?vB+7FBqV= zaBE1@N?`?N!^<(-y>-P=e_Y~hwRb^BvZ?xQw0 z^!=*WKNX`~$XnQJ;KU=t#;k6VzO{mC2k3pDkWgSu(UuV0F_G9TjKQS>%1KK|=nn2Vom8*w?olfAz-wb9oP1 zzV3PCkg<{0%{!64hD)?R-|wz4+rE0$+g%%8ywGN2i=fs9<8S19=K5aln_Taz-)eik zm-(*){C#%}A5m+(wFXD=N$1?!^-J&Ypq9f&9BSI<^phL><$BuZ zs%LBI_84#Eu{>(i$W>>1tK|5d|Io!xuXU5{d)4!}ZVv-q47MM9s9<5e;Kv`|8voek zc)aeZQOh5y9UfNKBz(T*_mCqW4}=a`v;Wnwn>(H;wn>Qn_GMtL*MmAo+omTcT=ULv ze1B{M$EfGM{X$;0GkU#wH{Wm42di3}8*?Z8NTXi+cl&Ti;i*o*%?-Uewcjw=-KNEs z+bv(NS)gKQ_?r98>6Oc=8J;^#@~;LK%-A*b`Jir7d$m(>KCjo^Y_ajGRcWWP98OPr zy25>DTPsVK7ad!er<>nxJpI7^z6lBoO>57yd4D^^P-SeL=9(9D(}ygsWty8cX#C43 zFT%h3L~MML+sJy&!pp^d&Q9u>e!tn+u^WUv{N~N$u3xwM`ivHb?rpM347}l(;?plB zyUK_cW}OvR3@k{qYCWc8P zuSa{-e?P(0VND<7#yXQ8MUNX>r&Wv9d|%|{@u4ru@bY@QH)o$6;MwYv!Vt$#A3iwL zpD+ln*bU(AGiXmvW^Nb6IX)%oX>d>w8^Y@=!DK~Vp1YK~nnvJX{7D|{9fa`Di{=K2 zqoMvEICXfgR0+iu;`3Oik`xV=y$BX&4UlWm0?1=s1PcsEUd=5kC9Z`$a(X37ZOE5v z0V;~QqlBW!JCT1H!*E9-i^RZX$#7nv^f!n7`}XfqaZpPj=3Zz+QH*D?wVMRN*tPsDe6*^ zqCqh-;yfd_-FH0YTZfl^7;YAxQ-fFMQ41bwhe$uv)-fd|#qrqDqpa@npFD=fVTK$_ zv@1!`_Odz*ZZ?17xcW;^A|Q42y&tdZJ};vE3! zO&?f}%D2%fRC}Q4ci0rD6TXmyD)@w7jbe{y0KZ`c!$VHDteK&%%tJ;hPAB^el!#t~ z2WNEn>HBs#Y62b)+!(m?Vk`emurCksa>plb73wwuCPJtV9xH6{(^3=oqbH6)7`|fx ztYP|Kz!#n+f9RL61sT>L7QZB-%-N1)R3NBAxk9c9X$RAJR}e*vg(;1+3=5VHLI2Ja zkrvlQ3|y=3&YJ~zB%v(HGyW>c$*f)(lGlR_%aS|_=4UGcY;yZjtzlkffJJ9^12P@->9BVh|l2A#&*qA%D>&NacN>h zIRl|vK`VEGHwH?O7K_g!EkTr*v>KEL&`@Tf@Xwyd{}zKVP9f!3dP$cB>VcltI+mnp z2_>F%{FsK2m%7eO4AOWKq3GwoJgsR^ok~*lPaB1nQQ2CHH zDC$*`qQ#FNvo~BF3ha3Y5701miW=|J5)Iy*WNqG)$DzEfiwBd@Ksr0+?)7RA>VIr8 zt3h>^L($w|HbuMc>knU;So1Eg-r@NC=|JA86YY6-Z{K#5#!FL_8DC3G=(5};LQ%*7 zF{nbBR--H(7xq4BLvuPz0adzWWJylNP~b^K!XYfmMbb~cpikzPpQr$ zQ|TD8>Eo_Wvu(9U$Hf{fZGG8}XEI)SSY~FesfY6G98Naq{_vyIl6HmPUTGcj8j@xe zAH2J&NB>VJZhkj<-mc)b(z3!EUy{ut(vCJuov>m>M@x=<=H!02ru8}0KhdRDAG-_d z^Cs1J)??|~Q#S4HSh#mp-nDFGZsEGT3#%;VH7*Q2e9kRSd3l@FkRqnd^ION8-RFM(dfsN4YuL%u>)p>89xO~c zf3nTH2^*(HXPMmEpj!Lcr=3<7U)SsUpD*ZG|E}%Q@7m*1QnECa$1ICIzBxHBAVE8$ z-~6zHW1sfidTORsqa6`Zr&c?!a9Q|jv6q7KIA6WNBDN8dXzz;qt8k#oU-bSI!cWGaQ(D7{?zCHLP@B3iJUGK50S5*4h( zV1ZmW>9P=ixJm2=6s0$b9A`vUc9Y2LzoqvzxJktQi@Z%@nfr3wtRg`E$Zl0`k)$^2 zJG7B+c1SMh`UgRV0VBg+OYB71kY8#B!~<&25}b!=|=265ZFi*SxFaD zNX{D)aDlxoCd7nx2F5=9PG`UwT^A{m-cdf)8i0A1XXED;xMy+_`9o0{6ymV}7zw0BxjA{^ zP^1L$$gh&;&fsu|dj|k_5h(V;;AQO|;MNnM7XV7w^a1D#-~*5VAnU)P@2H9qbuLB$ zpcX|`mlz9xN)S=sA*wbU3NQ=+^=c%7jXDBR>ECn!RLeISKnNi2-Lm~seoKb{(3G*I zBd=GUjG`g*X)kcW-rfzANQQ%C5vt9M#h~mtfF9!|eZvaAKY`JYLr+%d9}G@a^bX5B6pnL0cSS+ZpAWDkb=-q824_H1N&v$nCdU|6zJh`( z48%C`V%@`J%Ow*pd#aV0hUHAW^mjd=@{O3gJm$Uf6R*54g;22)lXco=1}mr(%sx|M zK8K6b3;pF2Sij7~%g}{n4t~VLrzXFg6E8ja%Aa^KRupM(LfR2dwT1xg;jarifswf4 zge&b@=G1Er!6nlw-Eir~N_RRJ${eQN&M*zPC&{xXU^+`U`AQOPP>vX}OL~DGRO}c` zjNPbCu#y`<&iQdohbR0MGc`e9iZcXp0>E*sJfXum6txF6mx6JFG$Ub z@|7Mx=~#dMiwomRyJ$b%;QPmLBBu}ogH^?iW+l?c1HihXq})M~Y%oxuvPK=~U(xd8 ze1N)U(O|dX0^o8HhdS6L5JfboLI(A>r~#mkj4A*c0966-Tkq-sxXIB1z%P|+0N`9x z3v3+#U4YsEbpZ4L^a1Ju)B`X8s1INWU z6M&`w%>b+bngg@|z_o!50M7R)nA-}#4glwAdjOR6MCnVEheV-99snP}3BVbk9RTJZ zPY}_@!>kSf?f`iD(h&gXe{{q7ANgKg0FYIJTl8)K-T>IPem4L21NZ&_z5spz{s032 z!TQl=^YgR$AJ-c=_oF|Y|3?By&;Qbsro6>? zb7&)y@#60otK=;#EX$FK{PByOe|d2;%X3&bV1t*;e9Vl8xU!e$*hz>f;#GdMW#{&v zEj4Bqmuegehg$hdHFv;wrsOQt*lUJT7i!XrGqSWH6#6F?Y8bI(%9WjvapWR$`l9rS z!p@&9(%O@SIfnYTgum;OZ2+vLgsjCYlVMq#;h#wGM)M6RBY53s0GbB&%@8l&un=e% zn|`AgYyyEk*?AxicQ=?cYFL-F1}l)s!-H)6vlZFkq1*=MUr?3?<1PRUG3A>7KY?F6 zQ3}%OqtpWaRm4No;I3zJ=!3_>S zUb{mi(m~2N!S=5+w1LVRb*b_*{5aXT3}cs@V7e=AEv4&0%0oA(bv$pX9QCnBSA6~J z2^9E*J6|x8j|&ZLV}h5gS%f`;fUGJ%D8(M6P){p z?F`KHKWu05{>!v)f3m~if}ZJ{TxU5FILS?%i({-K@Nrh7l_eBT1q6Ppn?A-=0CB%+LZFIS&&WAm6twp z{$k;4MI5wD)B%CY=j0np82@X46ZU;u z`Ccf$?_p&8FUl32LrD2^5sf=2143(^pfx|>`AZK-nU*vjK+a<1trcux{4s?1Fnu&q zQJ4BS2V7{OCi&wZp7+oxmKufF!4T5Nb4lz^y0d{t=(Ql{fuo5N{IH0UGJ^U;6a7)LE1c zAoE+9`TsqnT~{OkC7pf?$W0~ApE<~^vH>V>`M-eQVWM(Kr*Bia^hvsyHX2d1KeaxC ztAWUe2b@61lCd`w`XH`BY0ZG_v6z+&bRumth7~uCzqpm7Q!iioSay`DLem{uJbU#S zL{y%(gWNg+i*ZnyD;u-+g)c6Gwb}f`S!hJy<032%ZLl~5KL^(7B z94`$4i~t${AkGAA+(+R)68RvA!w(rO02%{W0yF_=3eXGyM+)?V7Cjd_XFM!Sf zT>!8hbp_}K;0=J^>GS~T3D66mH$WeNz5qS|{Q&v{_yYI=_yY_82mlBKz;#wIKnOr6 zKp4P4fI$F*0m1>s0ieWa6hJfp_DEbg!~qNe!1Cf=9_<9M69I+;i~tx3fcVj13jwg6 zB+@`7r2+nfvSF@7Fp~p>`L|@l+=v(`BuG{EZjpLa0AzSm)ArFtrtSF()C3~Hk7d9P z#DUKe+=+N8rh!Hvs7FLiyPwR?(lUtW*~tk}j|ie3G;R32VY?+DFIzIsdPGe1P~w1N zecPQc#Rm$prcL;$KAAdNiVqZhJ^s^FPzd(tsi1i5&r?C+*`mf@TTUIN%Wo{VEGSy5 zQS^x0v}*lQe4ya%fn?4HCVK% z)1mxQYAzmpXnkq0W^&r~QheZNNUtUhjN3Z;Y$-nQBc*ZKGmqr>@5->3TQ4ZW{LcD) z_-wC>rT9n$AO2QGE44#j^5n;XBG8{Q!tY->y1kS*P&oSB_;1N+iKWGXqSHgy2M@^n zdZCn5P_Vku-9a^)UwBnYDx<;22<)QeTn-2HcS54Bc@}RT6>G;EE3OjnB=k zY?@kG;Xkc+G>o_i1jf`fQPE&6w}J~kK`$ zn<7!rcP-VuYn%bq?1Kl*oc8lt9pnN``0A&9PH^K*rR3%nX?Fk65&Sib@}ya4t=uAVW|zGQ*$un74Jqh})apuk-8j{$dVd$QjsK>MpO|Dhq2xUOhYP=eiaFrO>KHr_}kr~=!`&Wb)Tx*oK{TqW{To<5Gg57T{?2rc#%}(L4 z@9qtG7(&u-0k}}ek3a0PTLP{H?8JuxrvUfnjHyid4Ir?kD1B#$7jm+Ly-(l2t^YJ% ze}D2%r!SvGY%KDB*_gld#-@KcqWY8m-%9@qufP7s=>N-?pOL#;v4<-BFZO>%{`2lD z)0h7rc6(3zT&Dii!1c ziHwMh@#!29)Gj70KFG(#sa-oK?@`Ttx<`aX4+(1L?9<)Ks-=yuQ&a!ga2U5Y- zc^(E2c&4oYdu_apsypn4>AkTJU~qSgQi(or29J6J+QD-e@Ciz}Ks^i5u-nEE&IE(9J}p6Nz!yR_C4R%nUQ@P=9K!1^SJtrzxSzo7HFBkqnFNnu z%Hr?PzWxb+6=kpoLRRpspgWMKB!TP+lN9&9$abK`X0URQd4#GQVku+xRZ8=?Fejy% zy1;)o2S*e$Ap0*l9+`2Uv8BCW`qt1Ry8yVt^DvNV!QM@Oi6HG${wJt<91;zfPx(NW#EpB#Y_Mt zif9R;(@F}x)g-jzk^qkM`Q*}te!S>!no{HYPiqz16C z9b-kGMK=R1q)v@7PbS_A_6d*mU33?~PPz4ZRG^zfb=zSg^Dg*wZX5#^)1#T}P4uVipM4@A77x`?{$l5{el2KS~V!BOZzx-RMPn$!kxhbSmMaYxGU1PuIBnQ}uI zP_MxeWyytd-8HGt;NBnHv7rJjp=PRr__;42_#%t}RR=J*;>-jL^h zIuv993v&kwvXF&o?oUyRSeS7G;HpU$=5hdtRI)JUQ2eDVOac^m84GhJgrb(SF!i8N zD_EGwffTipg*h?^gl1Wo8sQ)q!@>kbfaom?vp15W*0C^Z(G-=+!t@1}$a)rLGYAQ! zu`ou{C~5->6PrX)8(EkW(?L+2h0&c!QCnD;uvs9r#lqa0O;Oufm}YY*Y6lB52C8@` z3v&^wcoz#}3KhDWg&7JJx)(5+;<3a9Ds&&q&pfEm{VYELP@xA|es)8J9%5lsQYh*$ z3)34aG@XUnxB^7RSeWlnp_zaY!cYxu6?%{nROk`(BgQlW%rV4>+jAOJ@o^UB>l%v8 zJ3S#3m&Ee{st@;ml9(Gn_2sfK{Q+~1g;@-k^DN9Gz~r$o+ED2iSQszBTx4Mu1LhJ7 z^9V5K&uQIVxm~XS99YJYQiShBG$>^e7AQe_F51Lsl2Q?z^m>w6hyAgC4+=2V`N#f4 zfj<=ZLxDdO_(Op|6!=4dKNR>wfj<=ZLxDdO_}`|$WhkD6AE6DbqAM(n7htZkFjE0@ z4T++}@bV4-<~j@W6fief7(HM)<+CszfVs)SBm(9ZU?llj2bkL|%mu*QVPTXvQ`B7+ zrUhV-p&^MU0x;TLQ7Um^jUa~NHz_xqE!t?-45eu^lFt1sd+kkn)!dL*a z?=1@x514l>%u&F+XJLK-<^v1k0W89gEX+i}d}3iP0Om6bV*qT#FMx3px3PnO`HFtT zn0&x|WBC~dtj6ywKMMi#12BAXs42jFEN1yhLv|#vwIqIC?g0c~n6wP2YQUi6BBlzP zBaYlL2Fs*!g1#$If~gLe z`rybX>EQW@Ibe(dBa}&*p;Q>Sn}NHe3;%uyhD*Mh?tqe-2<2U?+67&n+I+*p`Z4@Fwd#QAymh`u*c60<8z44oj9 z1hWt(22U2|DNGDrEKFON20F7aS}^G2&67egHI}`i@1c~${Hh2vJAt30;!<3FP2W#> zQk=ssAL*N_8H@q-9){H35Q?wGt_QlFHK`C7VEbTRR2bUa225YTWI|PBB$imzQ465ak z#H<8U2CCtaV9Wu7(j5|vJ77@Fjs!CZFr!(R>3|VRVGO9PfEf!IA!!Mc`Il)snczMS z+%rWj#eg~wnDKx?p$2*5p*Bor6TqD>*IkqH2KR~JzJ$2rc;yF}NtlMHKUz`VIvgS> zeO^^0Phv<*prGJ54D&+lWT;L-4Lck3Ei1|cCQNj#4$ee^1VYeZ&{s(zYXhnWgupm( z-jJ5Tfg~YD1L`uk&j9z#n(T4{inu=o_nGiwP>X~wAuG_bHiyW^be3_)Fc^P4xX*@X zp$37Qe_}w&NsmLmgO`k>O98VOJ&1F-2{0J9e41KOjrBR?6X`QD zIa~{2)F`e{l0*EMS`8#vjOubIbX8<>D3T1KbGQavkd{rr7lv>ur7kyt`(|*@gkaKh zEO;m3Gr@fexTAQHv^!H5Kfrw(x{FE*Pk+oG3Y0~Gza)QHR^;Vy|C7FysDWI1iS8ZX zXV<#^4}5=Fru?n=-q$AoNcv^gKlA-%rqw_4{biZuXYPN^ob%(+KS2L7%P-p|q~&#F z-751g)%=jPQl|Nv)Bpc2|8JfC(`CS|YykSmm%iwy*4F#h*9_}`oZ5X&)N9Raxm725 z>i#v8*GyhNH*j;HdB>Igx~kW*xbVgzv2pmW?D!5yRf~%qKIYc5PD*_H z@QN1jUK9grT?xGe~$ur6s ze?+LNIc|Gvjj_>3kBy#w&9nCN=g)68(-`aeq57w@(Z+_B#Ya^=7i~&&Ola|GiKEit z$-?6rF3-a?y;H}oZ9iSnxt)nu==uD66F&SH>9xlosYYL;%;!%=`E=1d-l}Hs^*W}_-F51q%DZ4)dn=s$Vf=h@sUO6~k~G8G>bauba2+B?V3s_uMo z)}6r*LMDHEKI_V@j|ySe&)Ra8r`in|6za3;{=3HCsMtHX-@Bc^Y&+~#;8}I$)uX6S zOI>5*i=R!>`jVY`^ozbY8eH;tgG%A|$V&te<9>>mZKa;hh&WsOj-z#m8 ztg>nJlb8cf@*#Zwx;l@a&B5 zs2|O`_c(O4T94knxxz-TjtL#~!#;hOrLb$pJf6>mkLNz}i)ZQ&A3eHvTBF7qX&u)u zx&G;xW@G)ZZ>mmvGmPeaYV2${V|lGpmMc>Z+kYAoxX13soY(e2Yu6sQ+uCr(*pm~E z^lxWwYjFG6_i1NqkG*ntd$BKfnSSTzBcd}WX1%cLQs;}`oN;rt@~h6i|Di)b&oSH7 zFRI)x7_VsWRa;~20lR{2caJBf-nUWRG`Qx;vC&p<58nF`;&0+#^W?KplWu71qhKi8hxVRH_Un^`?d1BSAhhDbA?66|($<8_RRrv45 z73D;a-4lDNTH>INRrFQ;C+i37I`GYU*`^ihqaVKuZvJrAHsS45&4L`Otq-FY8;vo` zx$=ezRZuznJgG(PgT@UWx4wS8uAu?%^tq=t!VJ4bId3AXRaxC}Z-2d-?LuccHLHHy zml7!MuhGD{HNURUu>NN*txH|IZinHfEjv~YAF+P5?*Y4cjobCq*>_&otM%7$@mV!e zo(Ff(oa(&q3Z>C^#H{+}gRW(oHfy!u$J#c+8PhU^$2M5K$X2sY*|N!ox^n)!Usa#n zOOrDfsouQVw}0)Mcg~J`q3f@IQhT}GU|z&<>UfP)GcPuN)x_B;JH6_$ds{XXjTQ9j z8y6q3aN``;E0!JTolWe^QPE??N3b>oW?`9So-$4RuymQp!_^nKX8vMt>?w zF#KbKk;@ILtXHX%7Pn@(%lCwSTegK?8r*AvnNq;(sZq(h8V)GVu4R|suz}vh>uv8& zR9tFT!%iWq)`SQL-Mh)!HJOq}ti3guuF!r;5k!@(*_IBWyTN-M%YCVR(6}FwdyvBv6c8iWFnl3cC zKszP){Y*cVW_({Qg@fRKLAm;?ls@%hd)pr?$t^DfHCWk99XRvAS@4NJIX#Mk(4yk9be_Fz#&ny^YhHiQ5E^oV`=;Rofr6=rSid z!O(VodgG<1yH1?0(J18oV86N7i$>HMGeba%T~%FcA$BB-62%?fay&tO0J&96Gh|E&?jcr6=h{0Amc05R z?6YrV;hD=^;hsY?dGF(rljgI};0UN51neA}p1czyTUiIs%5fsijy{CDjJVy}hog9jTglr`kX8THJF|zGwnI2 z05cQy4*MY#5-c_eim=TSPjWf%0p8K4u=rGd*HzLP`@ynQ%WOIO6ot za3_tC^d=Sa79=8WK}37)CrTVZ{M3(Vm0Ea>LQZsv^qCQ>m^Ude9$$Kzs937Z0_*@< zl>!v;@~c4J8%HA1v>|a(qsWWst_@6*c)nZ%j9QVJK@y0e67_ey-c`@xyy0!I61it*U%*NzjcXH-vh-h(yALe+_5C!{Czc6R`XiExDJ&q3xF2l++mhXmmgGWp!O^+V# zd!Y)~bt8^J_DsstkBrb8N;{7Rd>zirc9<~KrxQ)pYS`5fkpRo)VK*rQ9K&=}+&K{u z)1y2+LchM6xD*+@8CEP}*@zbc;lc1UO;HbdkkB;6A;cL^;N1s0+jS)Q86of_HX)V}YOF|Ls{why_65lE9LVzv$oH5v zV1@Vh1T0{+`@2j~J^E*X*Z!fXMf?;vun73=O$kumzqK~}&wl-n|Mo}xkAaQ^yh!ma z{H_08`d?CiJOcUO1GR~ua{}q%M}7&)54@W3E&Q!~hF?;DJo@hc@B20QkzazY(|7rx z`x~@c3M&8iHUM=0gT8~-h(K)s=>7+m48ElSuU&lGQw=Qh11qe5Ndp$m{>J|k=(pHF z8V&$103HA+?J*EP0l)_!_$7@9h=J*cZ##BLe@P<;VhR9CfTzEtJqKbc0BV32zoY?& z{2Bg54FIYH|8*Y^{Qq}_N}w`l1HW)g)HTr7(b3b^2765g;|GKKq))ab9Nvcw5%r5U zN=q;z;PQ*dzHe_O0NvhTFtHRez^t}<_+yz_Qc~%mKJC3@K!{sl!CN=UMs$_INe1Gx zj*g|X&-&t10)2ciNW6#f_JmkIeq9o@3ovDox^LcSqk;`nBO;!2;^NT1wkGE7gNj~m z&U=K#Ou+q^b3q%WRn)y)MA<(;4jciWg{XF$8tjOJo&Ec%?E>L1-~ZFg-vHdW#K1N9 zwf><2{i6}?_Jt3Icgg~nLAFh{Cmb=8p_v#S@daWYq7h;M@6h8^N)=dvw}0FIFn(9i z567VP2h@Im>JX?Z{>TqO&laF}oj~DlneeBcF^-;hrStnvT8fbj>;A6VY~FZo;#80c^A2f<)bOYa1->=6->&%P2n zYu^<;LOavq#+>wUjP+Ma28U*xNa0$BM1R}+?G6k_ht?4$T!YpTfJEnlghc&B3t^%I z)$;&s6AQ-A#ncp$pH?cB_YFkP;Q}KTmf?ThlU~|kg~;d`5LniagLW1wcSO6va43)n z*>P~%^LXQ0!G_+w+gR=lolN4iYT4U~XCQ1pn>PPg_S{U}z`(Q)w~t3tM>l0@OPgS1 z{|!ojD4L6|uGO_F16ir?Nnt$0E-%#sE?Tq@e8FVICPfrzNX(Rp8K0exm0*R3cFN!N z`qz6Ol=83MKR@;NueCov_4iYMKkwgvU;FpB{V&}&Yc}6o${&e={s;W~T)~g_e~*9z z|Kt9b?l*mC|5jQ4NdHIpoAQ}|OU!SkgZh8aya{N0^rPob2IT$6=g<0^a(?R%f8_b+ z*{|gP?Eii~Km2_D`psr+Qr%s*g9{7_<38>} zzTR)#+qFqzj}tRk^tznv(f6m0e?fJlq?3F!YDA+?f^d%-$sbGlf%YR+-JD7SzFlS% zZ2CAbo2n9L*Si{F@iI)K)?rv`vaVW}t$w(k;rbP{R*4hF^Qr@-8cDsLPdUujtyYu> zQHKQgk1eTkNcqrW>}_|q?BotyDhu%s?8&2J0za6kA$=&qzIInoM4C6nS5MjHVyW^E zZT{N0m|(`{C|NySUL6E(?%=Yqwcip8_QHa0YPrkT7q4kGYU|0fF5?_sWDBDe{Kz6N zO!Yo-caRu z3du1C5lLM#GfEI8Z+g)@WV?MfX|zbJHKXR#&-7E(`B6U;d9Oo7Z?FxXVK}YXM(EQw z&IyS1r`v%J<8eW)uIhaQ<4r>r{I7C~gT0ERIOa`_j2EWE<}YJIt1%HaE4Nby=!WPF zV2apB7T&>#bKw_EKHqlXus_Osr!wxFhtUfg_g1Px1YR!L)PyxE&Y5oQBLH{iwvS#=UJ z>_^z~6yJPseMn;En%_5tN(|zxp)6Un+3gMXWm?r-PEx8^5k_$*p+WXM z>ON@|E?s!J2C-(4{#y5gA8m~ZB8Jr0A(^WVFn0^-F0!dX4&i(Jj#B{^0>cI-tV&3` zVY0_d@|S1Ufzr9Mdx{^pY1`&y-Pa!AncKb7+qPOfLW9-NzP1vme}cm7w`y7+_Yp1n zb4!RkB`e=bu+EtU)OA=sS>Lme$tvXQ=PZlIc0QcaL9IA^ZDY(ub*9t`%Q(=JBgfdS zHIj7**%TCJRSd&Z63NqP_gxOsYfd-?EjwQaPGO5;`J`(q_E2=o9?Efk@=Is^5}dA9 zx}Ux;z_1~TIeV#{7;1Hm`X+u_0b!g5hV3J~I=m3KjlpSrfv)6^0Rdc6HuNAG0?$Q~ zw6~2qtVp40mTp3^wL@Tav6n!A*toE3k|4%Y^keI|$I2&@F^rTmPEwG)udw;gin1*8 z*l5zXS?d}ewFE}9&v(|B*xD9_)eUJ`QC%N~c@rXm!Hfn=tX#UEo z&4H(c*lnsm^~@#3$Wu+z!sfnTjMlJ1)$(|fJ=@Wn!0jY zojaDw8qX@mTE@+TsNC<^;9^d8*o|&@pG0Ljudo<3VvQARo~8M!J}eaPH!KUql-j$L zJa`YIhI2Qt>i%KB>xSq(#Ue|={+^%U>a4(ha%FKmmZDqCD({?pRm6L@7Vp&$xm&p#w`{(0vcPEQvF}vko>do#3nrR;LaS!I5TF2b8L2f10XlD9$ zbxXij%#~r#;|(A?cLrWXn-7{`+kI)V*`DjnF(o`s@iE|+_iv&IWzXITg3N%t|H3~U zTDZpD9UyoE-V)feyPDmdg#T&2+)T!Mq?QboT2hR|-v6WP_#{}dxd-8?fc!nhX{(9N z@Y48;5jy(_ha{c}L)Dt&q8t=)pO2T_c?cbwaUoIkkOZ1vo2*l$tio0UN|>trT?Q>| zCDShLY7bEuQ=MyVa*CawGJ1VMD!NktE;7?WB{QCZXGJeUL_zbTyY)JM&gNy(!(3 zG+utP6+W+?Cvuw7Pg=a&XKM6Wc(&37mgHuM{!(NwzqrORcNT&_xMyDXWgk+ibh@eQr<_($ zP`+v{BpX_Iy$UO_w(7!sh5#KE2Nn02885Q+h^t6D`OXb-P;Xwu^2U{#Wi@|canx1j z@Dt=nP$Pl|&)r9|9T&dgRxE~yI#ul z!qk7|U-TLRMHnG2GLn(cJOAV%w}lyHRMABi#H$7; zPRL!yte_%-=I0BH={CtvMyOjGA^I(D-EeC{bfGy}h+!E9mUsBnu;mqNFgrIeqsKog z5YarB&is;FMW7s4vNU{A{k+Q~Y^c#ZBGs@U_h5Fa}3-VHug zzxCFAgJE}?48FpWG|%l?gV(Swjm)FHqhE*SIdFTXGqHxQcQU}27o$myp(u)H6C=_| zb0r^-@u)tv)4EtZ5G6HlPp^y)*_FDis|qelJgKHSgeazEo3Z2M*0mF$sn!q)Xt8zB z+oO}1X4Kx(Dr`Jhgbtw0HvTG!F&=3}>3H^Rcv>G>3+yY?B+Hs$}S-mBEtzWXX1wW6zv@`T?*z_hn_Ab0_sQ4J8fCD50$*oEK)$PhI)5R*@bTNISdd z*{Rcwi_~U0zN_Vtwkh(YVt!8P{y4Mb(78hC%@ll5iRdu=?aP6hFg2pD#n4KnnD(P3 zn9AKx^FwnnGdrv}{g=xFFr?|TC3+i+TDmkT3!l)sMt@-^&Da$dtH*Z#Bw>!t7&D=M z_Zm_DepLM#O|i}$PZsq@_UBetTLWyKN0h>sOK}fy+`0uW->6{txX)Lg5A#KYCp7X- zo(gfkL~f@3xTlz|8K@JDjbK$VAG!eX!LY}E#&4$>?ponNwvbV98^YE~&O}Pd@yM6w zQ3z7?D_8GpP%)0;1m;Pawmau*YO0go+wR?t8YFQ|ALeZ_GmE`ZOvK5j;eY0$&*;KV zB1}9PLfXMjN>`HkWiVOZ8B-p(GI$40@lzEwlKFcNv#)}SC_UQaP!-TfhBDs?(yAqQ z(K3{)oth4YA!}NDJ*$wAl%#bcH^7MNi>Q5ccYEBx@G!e3?rU&fv*~lSz&P=TDL8i# z3Y$0w+Y!SQD}w@pO6aZQ+}v;6DfcN@*T2%bk9F&U(%b@d@$`{6A#-HaNmEDyJrE6q zIv;1XYj%-E#}41~Yzi@3WE1$9j96z}h4zxUk9&M#yt>)sEHmJPR?QjOC6PRRQV`T% zY-{gT>)wWs%Z&EIwsOdUHM(+U*uP`{T#V7z3pt_;Ka2tSaf?m`!sMriC!x5-1vC1~ z!ltgh!LcuxmPVY!JzG$gYm zpfpd`9@Z<-NfmG>gO}3*`f5MVtb8uIOXU8kKOpa7KMcWLP9WB%Xg10!K z^HPoq!Wne_rM6s8bB5vs>3EbcJr8v_I|hYpQ%eIosqm{&`ht0m8*m%nUrz*UJ?Dz# zbkKf{q8j&PF2+r1_1yr5aG6jR344JbjMio}K9OWQXC|t1sa<~MYXO-w{5`TJD<{*7 zsF+Sm8pu~dN)iuWa%Hg?#Ba9UO_erci8d->9w|6^x+lFUFH>kP9zraBE#$S-{KEZN zCzx(_@ekb{5(Z!kV9r@|s;C&HYts zbPPM)L$Uhno+Rhu>acUomZG&$r)QspF4e!DrQg|_77QzE6ObO{2*ogxU)ZOaoC)QdhnAw zHa-C$G<)%b5Ol7p;|R166JROlV(G(s3!R2ZHIivx+7`7mA^6_i59BGMrcTFB9398* z1r^9{R?@?!^B$UL%4w69$BJJQhvoS3mF_nMRjO8c`Wa$rfD`mhjD#$KmLY>t+<|B*P9~xgF$KO z5+%clpU8zPtHI&tSZK`Wn_>dopLy83)dR{>%$~CDnZv8#PGrPliL33WOxQ^B``%tZ zZ@J?#nuzpcJE8%`=;?EgMR0F#dbdNez5>H~+?6o=xD+?`BwrGbla~QbIkhF0D~;|M zLEYRdLT_)zV~f~Jv7N?M;M~{xH8`vaf;;aup}GiWmwKh~f#?jgaf#P``wstu+Tq>~np%dXDzE9S622NT0gC zo2x%si#($oYbws&RGv;)m^f3M__*#%by59=V6S*#YiMZbtMhOXl&KP{WihdJ`-0c& zcfM}-2!UG6g!4p^on&Il5$u(|gayn9pSD5?3qK@E^ z@~Wju35NZ|vS4)vehto6yXD!lG>BI_rvYmS%?=`D7pIn+xjTonV$zEar+kPsS4KEc z>tmgICyrZFS1k^opH$oSo`>@89_D$9AP$<(K+V~o6nh#B1zAt+v|YfT5L_P_T(y?U zzMx)vi_>_a-UcyzQHy#*Jw+s+CP2M9r^X~cID;=y+ujZ~Yeh_bfo%nIq#17#EhXG$ z<8%m)xI^G`>xaK@<8{(!cx#h5pS%M0+!^mCKynktvQ~m|G}$VNTSu2N?6_YGW7w<~ zx2Jc6VN-g`kc$hPC31uORtIdvB(<}>i`Fnz1iFjHSpIfSdy)hu{BtA&lXvs~GfkGrbN z6}LIb$P~!{b|oWNpMaAi+%|B=s!qC;gK+q2jv7ftDvq^Sp-*S2V%V)Y!+PN~)}#Z& zaA9?mEuh)yP_P&xvMd094+m>ERd>h0P z7Ay;JXe2^pv}9XcXNQGeIu|V|zD0~lewyGu-Qk1UIPPvUb2VMKhwGTAjMOv68w7oQ zon-g~k+dOVgQ(o1Vu5cfY9iX)Mjn#rZnVY+R<6xH_VOq>mu$Xrci24^W=)^1uNQ|q zMbx8M`q9qx;252pM5k?XUyMxYhGwFJ9)9=*iKy%|T3T(IBRru^;K7>7Z-3CV{*hBi zx^2Z0b~#~y94)(x+zdOcnE2LyoTc2O0-S;aPUOA4Zm~Ce@eg3J+xCgWl%uiqX}IuL z#De6ZxvhjP>5?(A>|b%8zvo#-*J;nv>O*AAXX%TS8G-GTFdBh9JSVnLiuSWlj4imE zju3e+&KmZarDZ$y3E1ruky_!U@WyiT#lb zC3*HuH;Ovn%yU5zXv`Ld$5cKela#5fO`~9ys9Ei+?Ur=hw^l8hZOcCSN_7sVVd7!v z__>SHWuqGsQ;TG~7%%4^dCB*~ReFzhd!@v!nMIsNIqYIhlZo))H3n_(neu+{J)f8- z(7(ZIG+pttkPhYiGG-D#+L*JK*?mHLGUV0Bu#yD~Hx`bXPRF%O7qix5n`76{(&_!YM5C>eyE|v<=JiJc;V<(> zMg#62DrC9bkm|o&jU}PB&=#?K1$RG*z8lf>rB=VrCJ^!b%U3e4@#quNT{4!+$3~y2 zZymU>p!KuKMCPnE&)N*|@08dQ!{bD(LnC8EP9M)>b`&NTDtoBaY$adGYHy_I*R6-` zSUHMDiKZyu)84@Pl;2y4&7%7xUoCqC=BSs!+*WYcG$pf(=sX}`Lcf$ z7D6E6m5{j-y_3JPeuk|%W_&)u9g723GQMg!pSE$#=DOaq?v>uy+$xjHJeRwwx{cfp ziYy+zi<}1>QC*|~_B(niF>5M}83UKLxoxBGC@2e_uD0$Z=u5=mDG2BtWj);wC83NO z%5;$y6^9VZBYaAf>7pTeO7C_N!lHFLQ$N3El6qiPne|AY^x?Ktr+*b}?e$ImSI_oY zx>^Bj8lq>dr0inz66p#rDuki}_n*YI4X# z)7vUgqg95vXj3Et$QCQ3W?tGzRu%jCq}UQjC$_x{e~N>a?-CMyx$tJneZ|o6sY|{v z>{ranbx6#}LSg@rTh43jGkvov!jnJ?`vOH$$-j1A zp8Zvbf1U|~%(Ww9PB|>`YttodyPSsXt&c`lwAOoUWSW-5z|0IZi*P>IT|FxOi}LjL zRl8ZNf*`ZC)(95HB>i!9u5ClcQ9GB6lYkVJ3lX)( zVVW7L9=if z+Ry>F%^L3v$H3EI_565ZOdB(%)mJhxsuqCUnlq7fx`J|fY{C{a0fnlyV0m@$a)!w1 z)j)gq<1eo-i3SX~hF&{SeWK&OnR^rUIe8<}vzHD|_MvUe9F;*Y`>Py_QYVbc>#vq< zVC?$di&`b8`&?gO4vw=+Z}hu$G3w#5I`&=hpiMsR*`U-CacvRY#V27D(ASA0m4EPMGkHsEAlH&8G-XcIACOs% zIrMc&#RBJ~K_Fx}H;e=pWmF`*wX;)6%5VkD`>2a(<#zZhqcv*Yld=F|WK#1kI6*JO z=lwn}efYGp)?=AU0<-#M?AZEu&|22LO@M4@{G;&Q{_OYo*LlPMt1J3| z1(rV=;DP;Tpz95?=>dv=jEGG0;I4%C;Zp__oL?z z+DrDm{{IO@_(Sru{#*TL`~%ttykG(C52!%_?Ue)BMEZx}d%N>J{#B+wtNlR*j`^ed zR|oR)|5Ap3NBg4z$VLHm0zmld{m=A=^$$94PypGX`iJ4W-oMAc%JgUHA00R*s0_b4 z^lyGO2mT`e%U}N;`nLpRWBmVl|9{u}_xLw8^N0TMhwlHEe@N6n{LvqhAN|Y2;$Q1O z$o9`4Y5&2&0hew-FZ>sKYyYr>-}U}I{#B+wtNw%Ry!~kX&j<2;*Z;qx{ucuBy+H~C z!mssD_lNZVm*?-_WB;KNkc;={-G9IP`QzLE?ti2FZ;JmDKS1^Fd-xL%`@@I&E*lj8 z@Q?n)jQ@&!P+b7s3ZNL&C4*wnqtV~*{|D7?&?D>L+kXT-`~SWDN05Cn&|JW;?LUHS zzWj*&N04n4asUc|@Ae;`0pI`L{v!>LMhgIXUIvZXLE}b{^*WFRIgk|>766bn6_7f98lVQC7N8EG9-slB5ugd68K4EA6`&2E9iRiC6QB#A8=wcE7oZQIA7B7r z5a0{I5Wq0N2*4=7SAa2qaexVcNq{MUX@D7kS%5i!c>quu7lC*QU>RTqU=?5uU>#rs zU=v^qU>jfuU>9HyU?1QB;1J*l;27Wp;1u8t;2hur;1b{p;2Pit;1=Kx;2r>E%o{X# zfdGI6fC7L9fB^t`8wU9o29*)yLl^-75daAQ82|+UaAyeyxR3+`965gT_6s;T1j7Wt z0>B0U9QlC(uJ^zIcX?odOFS^Zy&TvR0DJ(DS6@N^A^?#6N)iB405Sk_015y~0FYPS zX8_Lur~s$|UI5Sl&;rl_&;u|4fXWK;stdBM!~(zyzy`n$zyZJs0P;Z#@-zF*mn;zT z0q_F|00;sI0SE(#0EhyJ0f+-g07wEz0Z0SL0LTK!0muU=0D$EG3_msSQv*LW@KXan zHSkjdKQ-`E13xwJe^~>dQNRPxSl|x^4d8bY0D%2@J#8I$!2W+66FQXn-nmMn!C20= z+%@M|XWyM|y)gkK&Z~(Ow`bgsM5j}Vk2!g4*2J#keGBXdHA?!xg#|{9QomS$i<^^x zMqS^+-x})vp89t&NIoaPU*&hv_tm+&4Yl4P8a8cZLKjaYe2_gv0DK8}H;G*{jGY=C zV^P_74xu_BVO&vznLy*Xem~?73H+Yw=mCfh^_62m7edLcKG~xzQJj~e;SHZOn-Qd$j4gr zIoa-OpB0Rj`&=%@X`{hDv;*^Gi7u+s1XMgcciWwA~vrkzaZ0TctHG z9ig+#?iKBb;rFFNdZQ?$76R@O#W<3zIp49l+L();$({)(Lq%D`#8w*BuZOR-&Gw$t z9z`D&5pe@4gG#tnl+Z5%l{Q!)p13t`xmOz3;YOzo2EDMj`;@SKET^B}kRW1*45{fO zjBMioPTAV5dlMQM=B|7nd5QC}cLLH&3#JrI&%k8_V;B^C5`0r(PREWb_xi8hW!-ZR zu3z){8Vr(|p&2nOgKs_0p5S%UC&`Y#dmkj=U%ShPJ63nMky~igE+84VzL$Oblth?+=qR`SSBMw zb;5X4wC=HS^_$vIr= zd*wwNAIyTdjB!s-FRKHvQRjeJ$RbV8kf>a?rZP1de9m<9JksPuJF{RUZeQBWF5FA^JnZ zIhW%hpCa#W5k{uTFoG{DVzq|cq*G|=mqN!-y=Xet?jnhMNwV)%78q{?&)0GgFnMIAgy6Den_+3hCA6PV!ZQ)u$KBk`|?IpNi?iQ%$-# zB-V_M1|przki}#8k7UG_5s0=_s$u;PNuLZVIYF`AvXbS@)0BLj*9qo!l^&d9N|Ae) zE^vbTAd*EXd%^E(JKC9QeqKETSIGiL6JH^z%fQp267y$SUZkP}y<9Q2*SKCn9ko3hhmCISP z6Zonr5Y_q4Yb3<7&C?L6Sr7Z;B?&GPW1Y#Rkf50Yyx9Al2W$qDl)-bu7uwKAEv=GP zaa{+fd)l4cW?!5O&#`cm(#aCpf~TSq5fU-c(QbcyGp^O`9_^XHxjk7%=BAdbqfP$2 z*JPRZ$rCMEhRcr;4VctzmI}hCtPVpIBf1y9Ol;4$_CPCS!-r{5Ev#GF#3smaEa@o@?*dUd;SC}s~G z{K*(WA~W_!ug9z(AUoL4^T*_`qTxx0q21{P4HTxc0}e&$BhHXCjV(ot#iF5!dZ%-&J@^bHVh4umJ_|Ys5XWO>Z3&c4h6HC}^{I-xh{|HQE-TK8XE*Kmt zMd-8!u=P)PkjVBAQc&2#BRU`C(TPs0p#*Yu&|zqWq3!mDGP?@zpM`vcc0mMt$c@`{ zj)OAn6ueY|PjXOXrMoqH$0Si%jXXMPxXtFg^^j@th>N}W8==7hlOEy&Klm0v4-Ti-CXR7VfzL1GQ9NQQ}>4Uw^IY+;@zUCe}Pc<3xp zRl04TIA=sKG_y-`W=F)#p8?n78ovhkM<>q8$bQ_3_W0T|v#vMkd7;V_ns!IB=R@5b zItK-b=>ZQ7*Rvi@O-5q}W3fnSt}L+3AwJt2{cFJ->MC}IbgSD5L46ZVyCkQYl$r*y z>x_{dq=??L*xlt(NXuJ`w>H;Atpj4SZ=3mI5q%6K<*h0i|c$8Dm z-Sj2in$O)CdA{|9x>p;7p`~^#7Qrj9SA(qmuz{0_RUFFG`%w5rRaN`uj*PUF@mnx8*xd5z)+5sDv~ch7 zz1|dZQ7vITJrt|bv|hh(jd((7*x*z%D8sO{{MG1g;IQ|ut0d65*du$(vr)fioubw+ znM>#-dZ$c6!-0U0prhcSG;(c-5wMoiVDWC zN9(UB=0lI3p!(E9eSYBxK@q^6h(NhyqR|f zz4;r|r!KRkFwI!q^s;A}`YVo0A8Y#(FoZ|kVo$PtS^IfdSd2$k`V^UrMptI?tJ3A( zM)5dE=}O5)btGhl4;Z~5^qM@8ZIU0Cwqr%WnF&lxKJ|G_vM_ei_SGQEDOY6f1hHj` zxI>)AC5y)MD-+kfFrqc-ZJ19gD`J6Q@P?K+n7iVyTaYS43zgNRv|cY1u?NpgHNYr$ zP{YI3Q2S~MHcwxU5a_0M^gF*WI%IPz;HO<4-7H5)4Gnmao2O%Kr!4w&{PLgE*Zgmk z^k474zSqP*7OMgM*Yz*`mxZVzA1c8-`ju`ACfq}>4kAO;EG*m2%NK|YeB#BlMX2dO z{{>4zmrnv&G!jfoPZZgPvWd+{*$mmAD$Ub|=`$Zl_&}VFh-5!=%BlAHZfACF^9d{M zz)sz4z1wGo#(0Bgl!4+CNML$COf64+8q3dXj3thGNRP@XF=gK?exk*sR4S;i_GPah z=1n~~_2bojsUAJc&6C-lOz`RBvD{fmd)~2Lf=12`X|y!es!tf|Tu_5!EQgj#Y1ym* z1qUZQM@Z=hwJn}|rlXxKtGY?m0s$2Ag)a*5ym;zbMf$aZ{?r||Z$y{R4z+>4ODf6g zQ%X&}*mc3k43b)Nm_Bw`M@!otb34xdl;Us_Rcj%3_Yk>9cF3No{dL;;Icg#GR5m;y9&IL;RbxJEF%D4z=F2M}me zYjTc{ZT1UJGbCm=#T*uv;s`|aC&B>QwoX2|aQ1^wbIi8l@ zHi_pd!A>PyiHcQhn(hdEP+HH5i@r(3sj|tiC4C5;L}Db>Z6lYZy{yBLS%7Cmd`uLr8DHgp~8j|RcIcOw*DPjDQ{?D zAyjv`X-RnSHMS%lrwO24mp1cF;DKi#vvN{|b0;2u>%j1CF6@E%#HKXO|@_l!ZD^{)w-y4p< zn_2sw{Lf=h`^63L{rOv4TZIlCN_o$~(_2;hZs7KqaOn(*czcFG`TfM8&%0@L!(BC; z6T=M1V84xF-n4lLF#Qg~%jjmdg4fbTTA0lT zE2khj{OOulh@ULHNj`7N7^;_0l~h~}^5G;i-t&8&axz38CL&ZLDSpDxvAA;f8^Oqm z7-U=@Tr_C(B%Bhz(lp<>#^br^&((WDlc@|1{k>f0Gd6>@=e6g_4tv!!ffKvP&b1GS zMmCaoEgseLXl@TyjdPhN5Oet`a9B;0Z$F6V-A2wnf9%hrr;b~ziOA{XCN*~b9G)q{ zgQ?`n;mbqYm#MD~m=B!I$EqmCGD~i`tT#@aPT#=exqZD_<*jSDRhP{WC?M18@%| z4yCL5^duaDBJf29@~bfurK?7nVJq22~{3JE=NGHv|S_|;Ie-L#fLf87Cvayo2vdYs~>!|95*@wzD?%*;lw1f<3FfOqVF?R ztE;QMVv#wcXOrr>E~93vtl?4D%zScX7I}T%8Dk{dkh#afiE?^NAZW#w;P9EVPbrNe zqF45IugXJm`Iz7&ScBQ?P~*VW(IIZSw8%92%zB7QYKy{jm-kvtz0O%ZerA&T^3^~| zw^LoAs}{1%^a#PyYN|!q`D>{YfzjeY^|s?w*tsYBuB^VjvaOxh=UYe$PHYP!@gK6{ z51&?>zznw9BuZ20xp4a2CHkyR5a_WP#oR4J<0rk#A=}>ZdG&U!!|6knmmi~AOy`hG z)!T5#cr5|D!U_au9}*Spf{_>7aZZ{a7SJCiR(yCf!BBs6xfVT3@8YJ9LI7!y{RFYn zmChs-{dkRSN}pitF8)i*!t7M8oqd8E9Zc`b({^_@pYVXPy>g#xo#mz&0od2>uemP> z7iz%?TvwI}Nkbl^Iw7_w0r@46c z)t+15Tt{Wc~luIEGxz?<1NIGW5o40YD z`GNL%oKKZ73Pl$qhThAOv2q8Mu|eaR)0yduX-_Ka;6TsYM?l6WKYhj-X>{J3Bk=qQ zSxedoZnNhcCLeFB8Mx(yd^r87Uc^_AO6_9vY z0@v_7b%beCRU;r3ap#r6^N#|QAW4>>A0q~p;daT&;-;FC^z$Z3`D}jDh>_5MwdF`o zi$g=)kogFa`fTBV&s)Yk^dNgA$0Fe(C~$z&<2@6eC!;=@<`JT(ey?igBAaoq8O^TV zxEV~U4(kUbUX);`wW!JBQA#j3Zfnm_1nj3G4AQL<>Z|pTjr$VZoT+SrR1Ot8gLG8J zY^l{pS!KD{yxXj)+nAor=a65@Z3X=|J<0V?FbOB-JULA*6sPegIy~!)UEW?2qDq5`?lc;Fq(GoCMsm-6#98XRtt+FTK-&%j_?y1YQ zN7xiGm5zCAq-j0<_I~&z;0cfW){KPMc>Q5_1U5>!UOCj`xw0&az3Fm3e;!AjuunpB z@+waZsT$SLV8QQrRp4QWRRY@)gO(UX&J(9L>8RTI-Jj4?2{qrj&`dQ}s)29xxr5;i z$mc=`U6i1%5H)4M-z>aG0moNbfbA9Vqr2eixCajWAbfgpcGiWWu={|z=;fUwQ3b}^tgH(hDxu|AwZMS0F0*yT z2t1`Xonfo1@v#KG+w*j%b{yE;3r)nVrq4SoY#Vh|90%%#Zub4j_27wWlBN^rPXcJ> zrp;8~`yRm9@*ub~g8vYnguHf255^s58>b z`zB|fYT%Npb&0>@sFDFjh6QZ4VZALZl6m}*XEtWhC zn42etz@`o0!?V#a+`o5V2QN)Etq4rb7&}FjwD!#Rt{|FU1J|-M&%g33*_S&wV2&2DRDntqYSDEL z@m@}Kh@-r?d@%3@0-6_E+;ieRwzW30>&@ZP%E%FaPru-V-`r)T#u&br+wY7yi0n^nf$T7e6QZg@$>P>?S(A;+n~Aug9R zydlZzV)!VDPBoXixK*K}Z9 zXA-9b2RAwO(;3AN=?n6ZbTKaLs0;mb4x&O>g`x-8dqSc^-_W7AosCA)kR;PjJM_e% zE?4SFBAC1+q-9qwf7Z3buH<-xNCyD1qs33T@xe_+=9Dhj0t^plNna^+3U0Y-BXM7GO!C3M_K9k3G&2iG=ygj$@@B7j zyZrL2G?4K8Yg0^WjOzo+cGE{e)Csh07EW7IMA){-TvjTNfaiPQF1`@EY!K%z$lK0f zi;)lPG=$U{N{WGKi}_{zF{q%?IM`=-c19fEn7l$6B`<1*at0b!9I`V;F4!F3s;;An zlN3R@91DOMMgYM2LD)rTx?^9~;UScdzrBWM1JnV7!neWZR{P+!nlJbz< z6BFv~pO{b|fdp$mH0$@{lJEFA;7JLe%GZ6brw5~_0RscWK@ta}Cr;uqS<%WoCOeJ{ z>yDIar1#!P58TW^*UO7!6B_SFjRFyv?(+NR6q)Pv9{Kp#(%j=q_XC0-eLl8N&}#OD z8w)s&5oz(gA;@_c&J7YndyW^R$I>d~%l8uBC)QgKkvbE;V@|LL93f=JaVVcE&Ao;p zq)8jkRcppO7#x`~>&ezkV*`xsvxwIk0_W2u^{AGb=${WOVZUdI#TX$-9w5UhCMpz@-1yU6IaX8eT#~iJ~~% zIs&H0A;`#Nfn!o8VJK3@jAjBZ#_Fw#AH*i8OPX2>s9ULAzBK$Y{aU{ik%y592yT?X zqC4bzi^U~~yn_SW2EQtkHb}i~m%!Q8UEAK(CH27&>?k{dp{6EL^yY*Pau?DCN9^&E zC#g;H!2n*Uxk^*2%&@et#FkyT?cbgON!z#lTC-aoB)CM(fD(@+3Yl zDdneNW3vphM{2;rp-^-GqMv?8Fa3!BxBg$_hhO}`f5JiF4^;Udg&+Ri`GfGk_=A5y zYkwvEpOV0L{P4&4gMT}x{LGv0{ELhKRCMI(0fe@L(q^$`Wy!r_VLLu)x7`s-3k z?qELT|3CN_d9A-sqHwT-q+<2@vIV$2&q9~qULJD%} z`rF{GTBqJOgT{P5c)Jb0hBu%|ef0)bn(c$9#~1NZj1`j|b;Zwhx#VB1(uoyQwz`Y< zgkwb3rFJb6^`>cD;SHa?hF&H2y8<#SeH+GBp|?gH=}q(cJT{Kg&}+B~(1}lLW(~s* z*C2Em@fVSk2Ezq`@VKVCc`QapBboeXsWN8|psSF(dOgf8vA%_i1E!s6=5#8<7g1XG zTkLx`!?(}&9l(sRwaTdFXAEWKpka$a1KXw!bn92`)+htYM`zPAw|ALb%38)FmI2!D z5)!)~a+NrIgJ6b_M^N}8D&OmKGxEf861~jUkLkL_Iod{!5)cwzTG7qK1TciH6ND6v z7^-X7?cN=BF@X3fWGftrxSK28+o3dQ9^oX?3=J;e)+&({;;-*4^L2ks;2R}Djf)je?gVSVNUL;c0{m8%$z z_GN2{Zw#rJcb^iJLq?;!t$*vv0i{pTC4|_p%;#;2xCXxlyWD#5k1*IMp;7*O$bp-n z>j4Ez+=L*w%K@s~X%-E?rvU8xLt1V6kaXj6rfqxQI!`ZV^Uw zRQeVWKi(|PP&vm`?TJQ+iV)|xD9A#)Pr}3*mDdeW=d*ea)k%Xbw3=cVEcBIQn-)4G z4@r%1L-vV+!Tx1|>^lVmuoxsDTPG4-Do2(88TRTDEg>@f6TKZ}*hogu{mcr9MH5&S z$`gn1iGqPFd1q>HiMRiqf?>NVU-A`w3Ov$Rw5)B|*a6)lxcR+k88yg93dKR@nPJ-Y zpO_byEqj%u8LTWeZ_VZ9*RBEP6+Xy=7WN@ezk_}K2<$BWXDXO;$CcETqzIt&g4bBovD+QLwo}6+lkKnY-MpN7M`>DdBR0aGs45}yUm(q z_XXpY3Kj<;j28rnJ|qU~2_fwP*KXu1$dj(vchyZ)eKe!i8_r`6m{60)bYj)u;nAilH<45EHr!$z!T|I*)HAb5q^Y zDht-5N`Y&~@(1NWb<~tO+fID5W8B4>leMdX%Tuc=ZMp7%tAng*#nPk9tFu)RqC1hw z^qIT4hwSa?8^;e>alu%2`@GFF{)`z20$gF zCkcCBBCjs8CX-kjKtP^UF}_K<_HslCK@r`Sa2o@5mGbi!#LsIuZIRyk5rS<@tzq{y zAyaUVhh#kbqeDcZdznm|=%90+bYsz<^T==TEkmOH`0Ze%v`PFx}-rNretlh#uD-_KjZ$|a11p-cORQnNzydb5@!xWjE4=jSc znO`D>UZjI*%pGN>UclZ)Ag~_FX0%Y}3Lvni(QbQ}RG{U>LpsKLGlDRjxNI7UcAtuh zDFzELddUG9Q`B6CWLCfhbQ3~7&-WL+vZb9Bu@Zo_SG{@Xj_b}m0xLAU3tX;$qS=U( zI8^8JejIB5_Ot<6j^o}r8t!Z1ZZRoAV1MO%qDBt}s;yvWqREElJ85{iRM{-?3}fZw zTo4iqf^AL@#}fA)pKJMB5c7RNe7)>38g^3eAQ+(@V+)+!qlrcgJ#^AsHucB>w$H;3 zgSMEru&xD=fevb}o$IZ^5HrMLJC|)t8l;ShRsCG@_k;$F!LNI}7L+|M#1q&3x-y*4 z<|*T+%n0heF*60pJ;Cae4W0j6AMcMu|Hgh!@hhP(NGX--pq}Wp(eCA1YO7hbEFe8y zWL4YU=m6{DUE;XTk?d5XH2T8(mt!f5FP%T+ODxPhH`b2AXtaA44kA=9B*T0A!rAPh zej{54d;eqNn-hlgs#m*ru`6+v_{@89d;Oi);k}j|(esx*M*xaY$vT@I5#Dqgp~hAle@>e`HN zqBi0`*;dP_#R2YQO1GgP)o&O20^B~z@1>Qx9*U+k95t`arz&&xZgJfr5>2!PX162k z1B!N^JI+vYEnudGmiu-i*1H`k$UG3Q!*COJNSj)O}={*qO(^xh>* zyFYCh2EIfVan%ogejxcS*Ao;|8)9>RUO%|^jhM$+pycXM>oJk{(kh{Ggr$LO-L$en z8J^F(qW!An^?alCvUdng{o#&QRfQ~cfNbsO#tdx&FD!{$oTW1A&r2(3ppMEyqn=gj*>jl&Fv6E2OeIB zmi&OO*YotfG>3f%aQ!YOcR(?A-p7IH3j0AJ{>ax+QT@q;*+ybVr}CwZ7ISgvI;TGJ zNe^Ef9JQwEvZRf3u_;mqUMJ4f3wF~2g8hJx=<&gP=0kF|Rgd4gxo8KX&EjaHjQg}- z4Zdj*fm1J+8J4!ttpiGC1!o5gAAHjbXW1pi52Z0zNwVDc?{6H-iKvm)XfgV5KC>e+ zD6)nchy9#_(pam~)QTR-ovhlk=xQr}!v020m!ZmJqHQxqJ-vQa>;q}skuoI?O;fQ{ zqT383T$<|#ky78@1jd%(51)&Z9$W^kzLB|7<_Zk6$l@o`ZB%r_NYg1v=SooREDt#6 zKd|4f9fur}SGdW=C=hMG(cgZZR@;yc1SJ* zJ+C9von?HOSw|X9Q)E^WOFUOGiENSEf$8KkycjOiH3i>;C3W*b+BdA1Drk4<&gKsL z_wUOFmLA?^6#xuDY_6`OQkQs8-k-(bySF%Q*kS2ctc&7%W1pqYXvJ7LE3AoGJq(o> z2R44D*PgGY5XpJs_9-sDS`|4C$gPmPn`~3lkY%+*pW>W!VlLj=Gy}}{0QlV4IV{qn zv6oxYYtAjp2J$9ujB!wvE$aXW7dlJJEt+(<0O2;jlEC8eY2KEY<;yQ!+h@i*OSv*f z8|IDM9~Tvql=BAc2OsP}{!wZBA-!SRU9y)%4D~J3@_L4Bvgz=(6&)I8vre=vz*_Xp zIqNy~!g=GOkc&@YXC=5xf>;_xo>If&97P@*%J+`hG8kFaH4*~58nvRlP9{}(>oSAh zlI>=Wiah?3x(%4lRy?THmUJ{Rk}b0*FzUx_QcR+cE>}gsYub5-k}G`n{iSB$OuJCB zk+y6i`YfsPX1(xcc@e1JROf^Kr~c{ZF?Trz4MaL@n^vzL_w2W@Q+l8KtQmd3Z~J3Qtbn@5vH*;P055LNQKHdbG@TYl3El}pF2#TULj(ii6xrCO-EWGRQMV5D!< zZ8|RyTuY!;(64$SOKeV*eeZCY{4(hhBmXuj_Zf;KeyBhnL#a$W>oY7@>dJX>vjXXj zO7i@rPgAgX;h(2)FrfEn+3T@92%6}+ttZDE6{RVn3tSfzunba==-sm-GVsLn+^4qr zc-)5$*up$uA{)d@W@}FkIu8zWc#LSniBXn4MkjmAH_#`Sx2H~{AU){L%GVC1@Cb0z z*J@sfdLEp_+LA#be(O|nyWpbi6AqLMbf6~~y{)}tn$0smJ6__YZGvXt>6cB#EJ`k_ z?sI&^O+DSDjh|Ggp=UF{vQl@LUS1E2&k4^o7`>jf3SLj&;8nv}wB?=P+&^=ZUYu>9 zqz{hAv=Y_ok5$9J*;xsu+-d0|S_L#X=*c-ROwH7}vXpDnznHRea?RN8H~JdqPW-N& zxf`>Gm|*k+rPLKFpk48;Z5Cvek=ZLG+i<*#oja;<$17SHV|DD$gzT@vEvqbElqmHQ z>5j6@pJaH=L)RA371BzH*?x71F9|l_Fxw5T%*LQHFHypomSDOaUfo=G=VBkKz`e(d@0`KHXfQzr`d@P9CO|eKQiX zyU3F}<$$O5ZFhW};-kqeqGz{m%vmCq63w)XfBiQUlPZt8{dYp7<5`4LpB6U`qqDCi zEmKtA_(q@2x^=@y`D>0%`uGw`$O_0tdA=+gFmOpxR;&VjZTYAud!Ns~D5;U$nDVl* z-hZOvv%zj5rWV%LIS}_on)$^EiYz~U%@P56GZ|8-tC z^%5esixR18HVP5IGQmozvw;mn{_Ru1$x;Q*WSi z*9RcIt+eiJT!7|+X^C|*%5*ZG>1(k%p3H&jS6CRPpvcL`ec53Dy3mm*S*PJ@;pD~J zgEJRCpRt_0gkdY4SPF*;w$0VB+)Q&K;`?^GaMc{*%Yw5cy+*x-uDYXpBy-mlCQHi< zvGr%_B{%g=CW}*~R}u@O_;Fb4N-tHU)9?4(a^v)}@;h`sEV?=zF=T-dK3$)alv${Z zvYNaTF6x@Xcrj14w-=u`n_ZJii{w<`QQ_?Yu#L6ybv{qn)N>@ik<~b0A&yH|sA1RC zx;>7uif;(?G?f|)sz(BkG@C>=r7etNceBUrDWBub(Y-G(zooHH7e7gPr-musjh6p)&U1Zbr&DGlebRvpRJB)?B+K+!`&Grqo z)vrf02{hhcH{N|#ay2+MRNOjle{dFpUaTH{aZA-R zYn*xuu*z6MG><*Lbk2V39qhb5g+b3Tpk}}2U44}k%u{GooMNLJKR`Wr$qIQx8$5T` z_B<2`V{(%Doc(2k$JH|x7EC}=;;DnZR^_+?YSZ0)TE;DTCv+#jhvV4HtXV;Wm3YU7 z^~GIrHk;`D(w6y9`Y1;+P8ObuGx=VlL3xZrb5dmFRc`D~g_H_=|Fo4mx9vrUBQmVd zKAk=SuN9WX*#{^4Z%P@&8#J_-JE(M4O%Rnv4_~nMHu1X`WlQsN$=6bAebH)P||y;xKrD^ERjbr z{I3s>sKaAfc7{&2ZP2n(sA~O+m$RuaH5WQJNwfN={LD;cJSYKqif*OdogwOKLnYiFiAZQeIcnFp4K`0@KMkJ(i0l1@O80nK}z8olX;W7VMMy|*~pb+2p= zFe9&YP@V8!Gd%BQp>_4<-ZNW8|90?ZXsF1bx$Av?we$(fXq#lOXhpQ+#N9J`IyZFl&E2k`(x4=pT<5~H zDv?fq_^AtR(uazWcEU+*s-kDwR?4|cjP7GoX&a$m*@c!Op{tFcZ^UzM=67lu{P`cz zGd3*t_|Z{_4!ExJ-Y2&g`|f1jb@pjXez(H{EWPXL**q` z!6LUn@mldMkz7@=pLK!Xq6BK-k(Ha|49|G})_F!pg=`-WYA=7qKAz6q(7S4g%L8T} zIwK0ZMU%t@E5-XW2U3YCYNeqW;~OCdh9YWgtL#^%7)LKNvT<)cMz`p~<3kYG^eRB7j%L^BV{Zdk-6OwK}P`@E8z&Yss8qXCO6ppz94KT=~WX((ngth3#fgx zPbQPgI)G4qQq$y335e5D$ab2%uqg-pARm*4?rbQUH}JxZu}Dsno_ua9BB`i;==J$& zZa`>f;nMX{+`+q`=L=3bRvl5Xr(uAupynKHfo%2CUYn23GDv1cJ5uf&*#sO#UEK>; zX~S91`z9{)XR-3pgEt$n_tfN@c?eFHtDIc!2CspX&@NJa-c+;`2WB75q?<$_OYyYA z%csw8-w|S)999xgM2SXN@-*b;_6+j6JcuRW!ySoQ$XIQK3G!uHVVVMss4&JwMfIP)UhI=FNU! z;$+<0eK!29t@*Fi^;ca47ZkgO63BTO^5DsxJ1OLD8yZZ+-zbMD@iq;d3KQ6 zH=9RhOdXeykhD|0-0bl#RganLbN9NaCdm72gL}1|p>^paedw&K64lt5%l(PeozAK0 zL*g=cssVULJS9cxo-iVTt;+(1cwV9GL{T~Lk3M>-jWXNq=R5Vp8>@~X*afmD4-o*D z1C;5M%+xbOEDb1HQSNSxEzZu}*H#}Q_w9MH4-{Blw=}8%ZW;$U=p&%V{52hU%+uK) zq9b1D0UQifM&eV&rdvF%$0|?N3HP!JzSS|BO-C;cXocRAn|-4}axs&WV{sm*^U>|` zoG;C^rzV3MY^HV4l|6OfSNq!*s0@M+RCpaI zt1HA$?4tE`@iFjKuu;<~JHZ98wiYFP3mt`}K&%uaNw7YV8Hs?9NGFb-^gU%P$J4V> zOZ~&Rn2}qr9dlF7pds-E{u)7n<@d|dF|Z2^5F99N{LGq2H<`}%dPlwbi6%yG1SvC*#a^7w>sj9v! zA+CqDDD&gq^;9>6^xQuWSbjRgH}pt{#6stbZP4V`0qdKO$Ue3E#>IU0g1x6_zFPf^ zXGh%IMrcN}xSM$}2xynHW#AAM9CN-OV728?(Q34kvpzJ~*{*dx4m((ONz_X=((@cQ zPTe6Et3|?Rv61;r1KVznWF#LRgp}?b-w~@qHNy7_1iXcBYkt`DvptrQ>P!E~DC;yF zJpNwzQi7zJX?R0MdS)O(@LfQeRIh@a*G9m34nBx!+R+1?2n8%7C5ew3K=Lp%;e7l) zcj&l!WQA1(0>xBEmkf={##80f+NyhTI8TLpp!~|oJ)$?2`0QYW*E_?@fP3$|6L!25 z*WE7_?Zr*8W4PR%=lx%dcn^k-d~C`5EVFcb?WY;aLSaS@R$v{!+0(RNU>mG7Cca~p z5s7%QKwSsskOIvjmWQS}cN!*}Yj`ZWedO~7xvko)LKJ#auo&g|LVyq1q751D`bg>3 z24@pohwot3EXRz|Fe56f>{&k${NjqZCzXQKW=@@CY-m5@LXsQS1ifJTL}EcU=(jc4}On46X2R*HP6IpF^wLycD;lW z*!+O%kemJl2P&zv0D3{)E(|w;DM;^w^RGl6wK)7*Z@kti^73k-AFgNuj!n&y2ws&!V~1Ard3n~lZBJq|6PI(X+@@M-esV>Y z#-71A1!3F8IesHHYgW#9he1KBN?f^FVaGJIf=t}gbC4)4vFjv@D2=^(6C*j$J_BYb=xP*c0El0ax@8_07C2M!mc zO?BQ%&1LYWpU}ppnh5SIc-Lm+zT4ICS@`|4tr$)hWe64lueO8?U?(`J;Yu9Oj+(?K zTn$aNocVdVe-TOi+n3(_i~py{0_*TJ5%4hxWP7kN z!kdiHR@kOCY}045+mrYSFRGK&wD}{rnXyiw6nTSctZW$5U}Cw5(5e?ItdUxK=UQWt z?stq&?kn!BXE5$P@z{^o2A{VXwVg#cVMB3q^$?gctTds3F`;GFp&Oy5-KV22c)*b5 zkQ1rq`6fQX1c?gep@c)~zUzR%;~|L^x6!--fJsd>+X+BN1bI`-3evLPODtpE^$;6w zmEgElAvDE4A?J#IOznafzBt(4oIrZynTM(`E0BdT!iY zwWb+-on zx}s%A_<7Ta6-J8vYwU~eE~G$-syAXPbzq)TGK4OAdSrYI9F8M$gHE6@PdE)Qza1W| z3`IViMZn-XDp6 z`Tvy!z<+Bs@Lxsv>v!L6F8=!6ANc=|F8@FI^WWw0d)o0Y*8YDw`Tw3i`k6RCtN%Zf z_wR}ON6P? CT~pZr literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/Issue134.cfs b/OpenMcdf.Ole.Tests/Issue134.cfs new file mode 100644 index 0000000000000000000000000000000000000000..f53f9b4f799023a10a089ad3361fbc38027fb8d6 GIT binary patch literal 2560 zcmeHH%}N4M7(JsI8U$M4!i_#cQAjIsQDB=0@c}e(QB(t(LA#dXQ%on_m$ z)Z)oTv_&sqF>`y&4tKfcKN1=D&=R~ zqC=SU6zMMe#5Fxl+|i@yMrb(k@BR>4g^0rS0_r>UnEFS364gMs@?YgdG@WU9 z;mp-GURVYGDT{>Ry_h${{p>Eg&X=6j9ZvEVj&XuhKU +/// Summary description for UnitTest1 +/// +[TestClass] +public class OlePropertiesExtensionsTests +{ + [TestMethod] + public void ReadSummaryInformation() + { + using var cf = RootStorage.OpenRead("_Test.ppt"); + using CfbStream stream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(stream); + + foreach (OleProperty p in co.Properties) + { + Debug.WriteLine(p); + } + } + + [TestMethod] + public void ReadDocumentSummaryInformation() + { + using var cf = RootStorage.OpenRead("_Test.ppt"); + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + + foreach (OleProperty p in co.Properties) + { + Debug.WriteLine(p); + } + } + + [TestMethod] + public void ReadThenWriteDocumentSummaryInformation() + { + using var cf = RootStorage.OpenRead("_Test.ppt"); + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + + using var cf2 = RootStorage.CreateInMemory(); + using CfbStream stream2 = cf2.CreateStream("\u0005DocumentSummaryInformation"); + co.Save(stream2); + } + + // Modify some document summary information properties, save to a file, and then validate the expected results + [TestMethod] + public void ModifyDocumentSummaryInformation() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("_Test.ppt")) + stream.CopyTo(modifiedStream); + + // Verify initial properties, and then create a modified document + using (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + + // The company property should exist but be empty + OleProperty companyProperty = co.Properties.First(prop => prop.PropertyName == "PIDDSI_COMPANY"); + Assert.AreEqual("", companyProperty.Value); + + // As a sanity check, check that the value of a property that we don't change remains the same + OleProperty formatProperty = co.Properties.First(prop => prop.PropertyName == "PIDDSI_PRESFORMAT"); + Assert.AreEqual("A4 Paper (210x297 mm)", formatProperty.Value); + + // The manager property shouldn't exist, and we'll add it + Assert.IsFalse(co.Properties.Any(prop => prop.PropertyName == "PIDDSI_MANAGER")); + + OleProperty managerProp = co.CreateProperty(VTPropertyType.VT_LPSTR, 0x0000000E, "PIDDSI_MANAGER"); + co.Add(managerProp); + + companyProperty.Value = "My Company"; + managerProp.Value = "The Boss"; + + co.Save(dsiStream); + } + + using (var cf = RootStorage.Open(modifiedStream)) + { + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + + OleProperty companyProperty = co.Properties.First(prop => prop.PropertyName == "PIDDSI_COMPANY"); + Assert.AreEqual("My Company", companyProperty.Value); + + OleProperty formatProperty = co.Properties.First(prop => prop.PropertyName == "PIDDSI_PRESFORMAT"); + Assert.AreEqual("A4 Paper (210x297 mm)", formatProperty.Value); + + OleProperty managerProperty = co.Properties.First(prop => prop.PropertyName == "PIDDSI_MANAGER"); + Assert.AreEqual("The Boss", managerProperty.Value); + } + } + + [TestMethod] + public void ReadSummaryInformationUtf8() + { + // Regression test for #33 + using var cf = RootStorage.Open("wstr_presets.doc", FileMode.Open); + using CfbStream stream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(stream); + + foreach (OleProperty p in co.Properties) + { + Debug.WriteLine(p); + } + + using CfbStream stream2 = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co2 = new(stream2); + + foreach (OleProperty p in co2.Properties) + { + Debug.WriteLine(p); + } + } + + [TestMethod] + public void ReadSummaryInformationUtf8Part2() + { + // Regression test for #34 + using var cf = RootStorage.OpenRead("2custom.doc"); + using CfbStream stream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(stream); + + foreach (OleProperty p in co.Properties) + { + Debug.WriteLine(p); + } + + using CfbStream stream2 = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co2 = new(stream2); + + foreach (OleProperty p in co2.Properties) + { + Debug.WriteLine(p); + } + + if (co2.UserDefinedProperties is not null) + { + foreach (OleProperty p in co2.UserDefinedProperties.Properties) + { + Debug.WriteLine(p); + } + } + } + + [TestMethod] + public void SummaryInformationReadLpwstring() + { + using var cf = RootStorage.OpenRead("english.presets.doc"); + using CfbStream stream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(stream); + + foreach (OleProperty p in co.Properties) + { + Debug.WriteLine(p); + } + } + + // Test that we can modify an LPWSTR property, and the value is null terminated as required + [TestMethod] + public void SummaryInformationModifyLpwstring() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("wstr_presets.doc")) + stream.CopyTo(modifiedStream); + + // Modify some LPWSTR properties, and save to a new file + using (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream dsiStream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + + OleProperty authorProperty = co.Properties.First(prop => prop.PropertyName == "PIDSI_AUTHOR"); + Assert.AreEqual(VTPropertyType.VT_LPWSTR, authorProperty.VTType); + Assert.AreEqual("zkyiqpqoroxnbdwhnjfqroxlgylpbgcwuhjfifpkvycugvuecoputqgknnbs", authorProperty.Value); + + OleProperty keyWordsProperty = co.Properties.First(prop => prop.PropertyName == "PIDSI_KEYWORDS"); + Assert.AreEqual(VTPropertyType.VT_LPWSTR, keyWordsProperty.VTType); + Assert.AreEqual("abcdefghijk", keyWordsProperty.Value); + + authorProperty.Value = "ABC"; + keyWordsProperty.Value = ""; + co.Save(dsiStream); + } + + // Open the new file and check for the expected values + using (var cf = RootStorage.Open(modifiedStream)) + { + using CfbStream stream = cf.OpenStream("\u0005SummaryInformation"); + OlePropertiesContainer co = new(stream); + + OleProperty authorProperty = co.Properties.First(prop => prop.PropertyName == "PIDSI_AUTHOR"); + Assert.AreEqual(VTPropertyType.VT_LPWSTR, authorProperty.VTType); + Assert.AreEqual("ABC", authorProperty.Value); + + OleProperty keyWordsProperty = co.Properties.First(prop => prop.PropertyName == "PIDSI_KEYWORDS"); + Assert.AreEqual(VTPropertyType.VT_LPWSTR, keyWordsProperty.VTType); + Assert.AreEqual("", keyWordsProperty.Value); + } + } + + // winUnicodeDictionary.doc contains a UserProperties section with the CP_WINUNICODE codepage, and LPWSTR string properties + [TestMethod] + public void TestReadUnicodeUserPropertiesDictionary() + { + using var cf = RootStorage.OpenRead("winUnicodeDictionary.doc"); + CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + OlePropertiesContainer? userProps = co.UserDefinedProperties; + + // CodePage should be CP_WINUNICODE (1200) + Assert.AreEqual(1200, userProps.Context.CodePage); + + // There should be 5 property names present, and 6 properties (the properties include the code page) + Assert.AreEqual(5, userProps.PropertyNames.Count); + Assert.AreEqual(6, userProps.Properties.Count); + + // Check for expected names and values + OleProperty[] propArray = userProps.Properties.ToArray(); + + // CodePage prop + Assert.AreEqual(1u, propArray[0].PropertyIdentifier); + Assert.AreEqual("0x00000001", propArray[0].PropertyName); + Assert.AreEqual((short)1200, propArray[0].Value); + + // String properties + Assert.AreEqual("A", propArray[1].PropertyName); + Assert.AreEqual("", propArray[1].Value); + Assert.AreEqual("AB", propArray[2].PropertyName); + Assert.AreEqual("X", propArray[2].Value); + Assert.AreEqual("ABC", propArray[3].PropertyName); + Assert.AreEqual("XY", propArray[3].Value); + Assert.AreEqual("ABCD", propArray[4].PropertyName); + Assert.AreEqual("XYZ", propArray[4].Value); + Assert.AreEqual("ABCDE", propArray[5].PropertyName); + Assert.AreEqual("XYZ!", propArray[5].Value); + } + + // Test that we can add user properties of various types and then read them back + [TestMethod] + public void AddDocumentSummaryInformationCustomInfo() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("english.presets.doc")) + stream.CopyTo(modifiedStream); + + // Test value for a VT_FILETIME property + DateTime testNow = DateTime.UtcNow; + + // english.presets.doc has a user defined property section, but no properties other than the codepage + using (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + OlePropertiesContainer? userProperties = co.UserDefinedProperties; + + userProperties.PropertyNames[2] = "StringProperty"; + userProperties.PropertyNames[3] = "BooleanProperty"; + userProperties.PropertyNames[4] = "IntegerProperty"; + userProperties.PropertyNames[5] = "DateProperty"; + userProperties.PropertyNames[6] = "DoubleProperty"; + + OleProperty stringProperty = co.CreateProperty(VTPropertyType.VT_LPSTR, 2); + stringProperty.Value = "Hello"; + userProperties.Add(stringProperty); + + OleProperty booleanProperty = co.CreateProperty(VTPropertyType.VT_BOOL, 3); + booleanProperty.Value = true; + userProperties.Add(booleanProperty); + + OleProperty integerProperty = co.CreateProperty(VTPropertyType.VT_I4, 4); + integerProperty.Value = 3456; + userProperties.Add(integerProperty); + + OleProperty timeProperty = co.CreateProperty(VTPropertyType.VT_FILETIME, 5); + timeProperty.Value = testNow; + userProperties.Add(timeProperty); + + OleProperty doubleProperty = co.CreateProperty(VTPropertyType.VT_R8, 6); + doubleProperty.Value = 1.234567d; + userProperties.Add(doubleProperty); + + co.Save(dsiStream); + } + + using (var cf = RootStorage.Open(modifiedStream)) + { + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + OleProperty[] propArray = co.UserDefinedProperties.Properties.ToArray(); + Assert.AreEqual(6, propArray.Length); + + // CodePage prop + Assert.AreEqual(1u, propArray[0].PropertyIdentifier); + Assert.AreEqual("0x00000001", propArray[0].PropertyName); + Assert.AreEqual((short)-535, propArray[0].Value); + + // User properties + Assert.AreEqual("StringProperty", propArray[1].PropertyName); + Assert.AreEqual("Hello", propArray[1].Value); + Assert.AreEqual(VTPropertyType.VT_LPSTR, propArray[1].VTType); + Assert.AreEqual("BooleanProperty", propArray[2].PropertyName); + Assert.AreEqual(true, propArray[2].Value); + Assert.AreEqual(VTPropertyType.VT_BOOL, propArray[2].VTType); + Assert.AreEqual("IntegerProperty", propArray[3].PropertyName); + Assert.AreEqual(3456, propArray[3].Value); + Assert.AreEqual(VTPropertyType.VT_I4, propArray[3].VTType); + Assert.AreEqual("DateProperty", propArray[4].PropertyName); + Assert.AreEqual(testNow, propArray[4].Value); + Assert.AreEqual(VTPropertyType.VT_FILETIME, propArray[4].VTType); + Assert.AreEqual("DoubleProperty", propArray[5].PropertyName); + Assert.AreEqual(1.234567d, propArray[5].Value); + Assert.AreEqual(VTPropertyType.VT_R8, propArray[5].VTType); + } + } + + // Try to read a document which contains Vector/String properties + // refs https://github.com/ironfede/openmcdf/issues/98 + [TestMethod] + public void ReadLpwstringVector() + { + using var cf = RootStorage.OpenRead("SampleWorkBook_bug98.xls"); + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + + OleProperty docPartsProperty = co.Properties.FirstOrDefault(property => property.PropertyIdentifier == 13); //13 == PIDDSI_DOCPARTS + + var docPartsValues = docPartsProperty.Value as IList; + Assert.AreEqual(3, docPartsValues.Count); + Assert.AreEqual("Sheet1", docPartsValues[0]); + Assert.AreEqual("Sheet2", docPartsValues[1]); + Assert.AreEqual("Sheet3", docPartsValues[2]); + } + + [TestMethod] + public void ReadClsidProperty() + { + Guid guid = new("15891a95-bf6e-4409-b7d0-3a31c391fa31"); + using var cf = RootStorage.OpenRead("CLSIDPropertyTest.file"); + using CfbStream stream = cf.OpenStream("\u0005C3teagxwOttdbfkuIaamtae3Ie"); + OlePropertiesContainer co = new(stream); + OleProperty clsidProp = co.Properties.First(x => x.PropertyName == "DocumentID"); + Assert.AreEqual(guid, clsidProp.Value); + } + + // The test file 'report.xls' contains a DocumentSummaryInfo section, but no user defined properties. + // This tests adding a new user defined properties section to the existing DocumentSummaryInfo. + [TestMethod] + public void AddUserDefinedPropertiesSection() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("report.xls")) + stream.CopyTo(modifiedStream); + + using (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + + Assert.IsNull(co.UserDefinedProperties); + + OlePropertiesContainer newUserDefinedProperties = co.CreateUserDefinedProperties(65001); // 65001 - UTF-8 + + newUserDefinedProperties.PropertyNames[2] = "MyCustomProperty"; + + OleProperty CreateProperty = co.CreateProperty(VTPropertyType.VT_LPSTR, 2); + CreateProperty.Value = "Testing"; + newUserDefinedProperties.Add(CreateProperty); + + co.Save(dsiStream); + } + + using (var cf = RootStorage.Open(modifiedStream)) + { + using CfbStream stream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(stream); + + // User defined properties should be present now + Assert.IsNotNull(co.UserDefinedProperties); + Assert.AreEqual(65001, co.UserDefinedProperties.Context.CodePage); + + // And the expected properties should the there + OleProperty[] propArray = co.UserDefinedProperties.Properties.ToArray(); + Assert.AreEqual(propArray.Length, 2); + + // CodePage prop + Assert.AreEqual(1u, propArray[0].PropertyIdentifier); + Assert.AreEqual("0x00000001", propArray[0].PropertyName); + Assert.AreEqual((short)-535, propArray[0].Value); + + // User properties + Assert.AreEqual("MyCustomProperty", propArray[1].PropertyName); + Assert.AreEqual("Testing", propArray[1].Value); + Assert.AreEqual(VTPropertyType.VT_LPSTR, propArray[1].VTType); + } + } + + // A test for the issue described in https://github.com/ironfede/openmcdf/issues/134 where modifying an AppSpecific stream + // removes any already-existing Dictionary property + [TestMethod] + public void TestRetainDictionaryPropertyInAppSpecificStreams() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("Issue134.cfs")) + stream.CopyTo(modifiedStream); + + Dictionary expectedPropertyNames = new() + { + [2] = "Document Number", + [3] = "Revision", + [4] = "Project Name" + }; + + using (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + using CfbStream testStream = cf.OpenStream("Issue134"); + OlePropertiesContainer co = new(testStream); + + CollectionAssert.AreEqual(expectedPropertyNames, co.PropertyNames); + + // Write test file + co.Save(testStream); + } + + // Open test file, and check that the property names are still as expected. + using (var cf = RootStorage.Open(modifiedStream)) + { + using CfbStream testStream = cf.OpenStream("Issue134"); + OlePropertiesContainer co = new(testStream); + + CollectionAssert.AreEqual(expectedPropertyNames, co.PropertyNames); + } + } +} diff --git a/OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj b/OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj new file mode 100644 index 00000000..59b58f11 --- /dev/null +++ b/OpenMcdf.Ole.Tests/OpenMcdf.Ole.Tests.csproj @@ -0,0 +1,54 @@ + + + + net48;net8.0 + Exe + 12.0 + enable + enable + + false + true + true + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + PreserveNewest + + + + diff --git a/OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls b/OpenMcdf.Ole.Tests/SampleWorkBook_bug98.xls new file mode 100644 index 0000000000000000000000000000000000000000..063c04f5b831dbef016db7bae0fb5047488121a0 GIT binary patch literal 19456 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 literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/_Test.ppt b/OpenMcdf.Ole.Tests/_Test.ppt new file mode 100644 index 0000000000000000000000000000000000000000..4b5a7692c487ed3675640407315cdb4240fdb68a GIT binary patch literal 174592 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 sTd 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 literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/english.presets.doc b/OpenMcdf.Ole.Tests/english.presets.doc new file mode 100644 index 0000000000000000000000000000000000000000..3fdf7297b22e4ab8f3bbb14f380655f6ad1d24a0 GIT binary patch literal 9728 zcmeI1Pi$009LMLCZMy}8Qc6WYSxN;&ETxDbpq2tHR7wj)@h`I5UFhoWzPh_j!4vWS zK_mW2yqJiJClgaOp4E%tAd!O}ys0-650w}b67Bl=&f6zTk?pnu0kgCDyqPyU^PAuN z=FgiyXTF{D<9nYh_{ofOo6Iybma8#U1@R8yTK}Cu1!2XF<#IVcnqb*oM;7=iudCWH zKqFRy+PoTqPy^Fp2F!#TU>3{<<(ZTBucIoLRV}cVM_(&s(4rn-$vMpTDOv{`D=;_^*SxP!IE<0d9o(a1$(mg|G;2 zhFf4UgkT9Yg352gzZI6ja##V1Ux|M^tb#kB8CF9JtbtZo3wOdgxC_?92DlqG!Y0@Z z_rSeyAGE<1Xosz^4YorEbixkU30-hM?1FCC4L#5cdtfj0!9I8Z9)$hy5IhWzz@zXO zJPrro2{;I0P#yd6qY%r#pKsAC8*(Up8WX%qsjATz-Iy=!({!l)y5s$6H{%}4hW5MZ z=pEbKr-qWTRMvhIy89Hj&F#0V0)?A-vHv%3Fu&jZ>C>K|Vo%3gMifg@^uIcK+H_VS zkJ>Mimvn$-%5S*}gU`2YH}hNCB(ZdYclF1*Xuac@HBHXklXTjXr_7cnv$DydPqnW! zJzW8_2ex)KnBVVCy5!cgIfw~o@7xg?9 z>q$Mf*fEWc`f)YAI7aHIi6i?kF*h(~=bjv;;zvvswVscRN@P@0*}FlOQ=LoPM*T;30K z1sQjZ$ZdDph^?kJVKYdIxUIePt4G=qtKUrYt3$8(=B}WltYLzW2c2cMhI{d~V%bT} zqEH=CZ z`Qa*pxi&?3BMD!Vq?{s{Fl?XEqN=muxTE-q+&&DGq;eSyyBmKPUmtCC=oFdPA;&MQ zn5;e;ENLR-)G%1PthCa%(Ba~gq&#tHtoeQdrzVe}sk*n{tRW^y#l#r|nkb9cX?=;> zsp}+cX_Cw&tbD$~i)eW%4Q0^``3h^@Syh^)y3wGY8oEeusig+_!|12sC*9VSEJs=# z&f9!l;5dV8V%JUj^SRpT{!Z{!=I=9fjR7lQhH;~xb_$w5|C%t{q%}EKATpQJqOZ^enZ$?b>k&KM zYph=!Q<(lTg~!-&IRDJH=Y^`H{`kVXXV3Kv)PD4O(A>WKvmaFB&HOPFU;ht)j5i3= z;CWz%H}8OE^%2mFehRALSJ2e0%q*GAOnmK&=jUbe*|GcM7JSM5g+|a?S84Xevx!(o zJeG)haoR`s4Mm=e^=I?3UcBTF4#y&yY%ERC=GT7v{@BrB7Fd~1cY9iEM9I~fx^F0% z3}-_-Q>2e&vhmd6RUtPWO1XS;l}2^l?WU9AM03>5CT*N_(|WlLjvYPw8tU3Go^@?{ zuJ|DF?&vr68(o|3`8l)lJ0-iw{raKfbcbp%+i2Yv-oIYQ;zOse<1Di^>?Xpg4PNX8 z`D+3Vrd5^8JLy%-<#MxayGRj*)i!>3e7=`$#G$-M-yff<@Lg%@3nL~7KSO$1_r<*L zC;y!HgYaM78_1Z7n0a*F2pNazvMdVC3BuZymEHf-0&mg2>W5d`RhS>Hc9G@s$`&YF z;F_{P*sgf4DV55~Tulq)*Lhd7$Z}cNj0GJ3GIFA_5tbf5QP~8oz1+?mA8=X*rO#U4R?b-`UzVhnsM_bEXpu6gW;5+^A@6EMx-p9r{mX2rGJlp-T z_Um2lKq_^#-OX?wsC~4~2inu3SBf!bepSfyt5OL8bT!Lij;-E>MZex(B~TqOtdL)c%}3A+2&UCJR^#Je%`kMF&-|Mpx# z`Ai7*ET+`*my$lA&Vt)-%ZhS8=5l;4O=4)E`=Zj^f+^JhHbnd9PL&sxJUM%8m_qpx X%0E?F^-4-*8$SOiC4=sp{d9i=@PHCg literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/report.xls b/OpenMcdf.Ole.Tests/report.xls new file mode 100644 index 0000000000000000000000000000000000000000..7b667a5077161aeba1d12f02944707b21581b2f7 GIT binary patch literal 16896 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 izF0CE{{zz*6^j92|LHBJV3B1?{{I1vYc3lA literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/winUnicodeDictionary.doc b/OpenMcdf.Ole.Tests/winUnicodeDictionary.doc new file mode 100644 index 0000000000000000000000000000000000000000..2ba5153c78913c14c548d70d188ceae510d70845 GIT binary patch literal 26624 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 literal 0 HcmV?d00001 diff --git a/OpenMcdf.Ole.Tests/wstr_presets.doc b/OpenMcdf.Ole.Tests/wstr_presets.doc new file mode 100644 index 0000000000000000000000000000000000000000..884da6af5fb42bd42f8f7b62213d15d1c4008337 GIT binary patch literal 27136 zcmeHQ2|Sg{`=4{{yCezK2}O%7g}MnvS+azbRtE=1oMTI?QqjWgrp;|pN!k@60>z%)B#4+0{-} z+YV|yAYyJfks$9|NkRl_ zr?C?4$Za*>nNAcK?%xjl>_B<3fvp9fl6R6Ynh@>2

J_Lj4| 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 QxS(;mFa>@2|3=_{03O{PXaE2J literal 0 HcmV?d00001 diff --git a/OpenMcdf.Tests/OpenMcdf.Tests.csproj b/OpenMcdf.Tests/OpenMcdf.Tests.csproj index 2736a5b8..dcba798f 100644 --- a/OpenMcdf.Tests/OpenMcdf.Tests.csproj +++ b/OpenMcdf.Tests/OpenMcdf.Tests.csproj @@ -3,7 +3,7 @@ net48;net8.0-windows Exe - 11.0 + 12.0 enable enable diff --git a/OpenMcdf.sln b/OpenMcdf.sln index 0e7f8a06..d8639c4d 100644 --- a/OpenMcdf.sln +++ b/OpenMcdf.sln @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Ole", "OpenMcdf.Ol EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructuredStorageExplorer", "StructuredStorageExplorer\StructuredStorageExplorer.csproj", "{D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMcdf.Ole.Tests", "OpenMcdf.Ole.Tests\OpenMcdf.Ole.Tests.csproj", "{34F153C4-3EFA-4D6E-B860-AEE300CCCF98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5DDCC19-80C4-40D2-AEBF-2DA1CCB1D543}.Release|Any CPU.Build.0 = Release|Any CPU + {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34F153C4-3EFA-4D6E-B860-AEE300CCCF98}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenMcdf/RootStorage.cs b/OpenMcdf/RootStorage.cs index 86e2c3fd..e6a45cd4 100644 --- a/OpenMcdf/RootStorage.cs +++ b/OpenMcdf/RootStorage.cs @@ -58,6 +58,8 @@ public static RootStorage Create(Stream stream, Version version = Version.V3, St return new RootStorage(rootContextSite, flags); } + public static RootStorage CreateInMemory(Version version = Version.V3) => Create(new MemoryStream(), version); + public static RootStorage Open(string fileName, FileMode mode, StorageModeFlags flags = StorageModeFlags.None) { ThrowIfLeaveOpen(flags); @@ -91,7 +93,7 @@ public static RootStorage Open(Stream stream, StorageModeFlags flags = StorageMo this.storageModeFlags = storageModeFlags; } - public void Dispose() => Context?.Dispose(); + public void Dispose() => Context.Dispose(); public void Flush(bool consolidate = false) {