├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── dotnet.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── OOXMLValidator.sln ├── OOXMLValidatorCLI ├── Classes │ ├── DirectoryService.cs │ ├── DocumentUtils.cs │ ├── FileService.cs │ ├── FunctionUtils.cs │ ├── Validate.cs │ └── ValidationErrorInfoInternal.cs ├── Interfaces │ ├── IDirectoryService.cs │ ├── IDocumentUtils.cs │ ├── IFileService.cs │ ├── IFunctionUtils.cs │ └── IValidate.cs ├── OOXMLValidatorCLI.csproj ├── Program.cs └── stylecop.json ├── OOXMLValidatorCLITests ├── FunctionUtilsTests.cs ├── OOXMLValidatorCLITests.csproj ├── ValidateTests.cs └── stylecop.json ├── README.md └── dev.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0003: Remove qualification 4 | dotnet_diagnostic.IDE0003.severity = none 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: OOXMLValidator Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | env: 13 | AZURE_FUNCTIONAPP_NAME: OOXMLValidator # set this to your application's name 14 | AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | os: 21 | - macos-latest 22 | - ubuntu-latest 23 | - windows-latest 24 | dotnet-version: 25 | - 8.x 26 | runs-on: ${{ matrix.os }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Setup DotNet ${{ matrix.dotnet-version }} Environment 31 | uses: actions/setup-dotnet@v4 32 | id: setup-dotnet 33 | with: 34 | dotnet-version: ${{ matrix.dotnet-version }} 35 | - name: Install dependencies 36 | run: dotnet restore 37 | - name: Build 38 | run: dotnet build --configuration Release --no-restore 39 | - name: Test 40 | run: dotnet test ./OOXMLValidatorCLITests/OOXMLValidatorCLITests.csproj --no-restore --verbosity normal 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: OOXMLValidator Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: [main] 11 | paths-ignore: 12 | - '**.md' 13 | env: 14 | AZURE_FUNCTIONAPP_NAME: OOXMLValidator # set this to your application's name 15 | AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root 16 | 17 | jobs: 18 | publish: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | build_env: ["linux-x64", "linux-arm64", "osx-x64", "osx-arm64", "win-x64"] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Setup DotNet 8.x Environment 28 | uses: actions/setup-dotnet@v4 29 | with: 30 | dotnet-version: 8.x 31 | - name: Install dependencies 32 | run: dotnet restore 33 | 34 | - name: Build/${{ matrix.build_env }} 35 | run: ./dev.sh build ${{ matrix.build_env }} 36 | 37 | - name: Artifacts/${{ matrix.build_env }} 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: build-${{ matrix.build_env }} 41 | path: | 42 | ./OOXMLValidatorCLI/bin/Release/net8.0/${{ matrix.build_env }}/publish/OOXMLValidatorCLI 43 | ./OOXMLValidatorCLI/bin/Release/net8.0/${{ matrix.build_env }}/publish/OOXMLValidatorCLI.exe 44 | 45 | publish-test-osx-x64: 46 | needs: publish 47 | runs-on: macos-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/download-artifact@v4 51 | with: 52 | name: build-osx-x64 53 | path: ./test-ci 54 | - run: CI_SHELL_OVERRIDE=test-ci ./dev.sh test osx-x64 55 | 56 | publish-test-linux: 57 | needs: publish 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: actions/download-artifact@v4 62 | with: 63 | name: build-linux-x64 64 | path: ./test-ci 65 | - run: CI_SHELL_OVERRIDE=test-ci ./dev.sh test linux-x64 66 | 67 | publish-test-windows: 68 | needs: publish 69 | runs-on: windows-latest 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: actions/download-artifact@v4 73 | with: 74 | name: build-win-x64 75 | path: ./test-ci 76 | - shell: bash 77 | run: CI_SHELL_OVERRIDE=test-ci ./dev.sh test win-x64 78 | 79 | release: 80 | name: release 81 | needs: [publish, publish-test-osx-x64, publish-test-linux, publish-test-windows] 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/download-artifact@v4 85 | with: 86 | name: build-linux-x64 87 | path: build-linux-x64 88 | - uses: actions/download-artifact@v4 89 | with: 90 | name: build-linux-arm64 91 | path: build-linux-arm64 92 | - uses: actions/download-artifact@v4 93 | with: 94 | name: build-osx-x64 95 | path: build-osx-x64 96 | - uses: actions/download-artifact@v4 97 | with: 98 | name: build-osx-arm64 99 | path: build-osx-arm64 100 | - uses: actions/download-artifact@v4 101 | with: 102 | name: build-win-x64 103 | path: build-win-x64 104 | - run: | 105 | cd build-linux-x64 && zip -r ../linux-x64.zip * && cd - 106 | cd build-linux-arm64 && zip -r ../linux-arm64.zip * && cd - 107 | cd build-osx-x64 && zip -r ../osx-x64.zip * && cd - 108 | cd build-osx-arm64 && zip -r ../osx-arm64.zip * && cd - 109 | cd build-win-x64 && zip -r ../win-x64.zip * && cd - 110 | - name: release 111 | uses: softprops/action-gh-release@v2 112 | if: startsWith(github.ref, 'refs/tags/') 113 | with: 114 | files: | 115 | linux-x64.zip 116 | linux-arm64.zip 117 | osx-x64.zip 118 | osx-arm64.zip 119 | win-x64.zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | 33 | .vscode/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # Visual Studio 2017 auto generated files 38 | Generated\ Files/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # Benchmark Results 54 | BenchmarkDotNet.Artifacts/ 55 | 56 | # .NET Core 57 | project.lock.json 58 | project.fragment.lock.json 59 | artifacts/ 60 | 61 | # StyleCop 62 | StyleCopReport.xml 63 | 64 | # Files built by Visual Studio 65 | *_i.c 66 | *_p.c 67 | *_h.h 68 | *.ilk 69 | *.meta 70 | *.obj 71 | *.iobj 72 | *.pch 73 | *.pdb 74 | *.ipdb 75 | *.pgc 76 | *.pgd 77 | *.rsp 78 | *.sbr 79 | *.tlb 80 | *.tli 81 | *.tlh 82 | *.tmp 83 | *.tmp_proj 84 | *_wpftmp.csproj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.svclog 91 | *.scc 92 | 93 | # Chutzpah Test files 94 | _Chutzpah* 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opendb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | *.VC.db 105 | *.VC.VC.opendb 106 | 107 | # Visual Studio profiler 108 | *.psess 109 | *.vsp 110 | *.vspx 111 | *.sap 112 | 113 | # Visual Studio Trace Files 114 | *.e2e 115 | 116 | # TFS 2012 Local Workspace 117 | $tf/ 118 | 119 | # Guidance Automation Toolkit 120 | *.gpState 121 | 122 | # ReSharper is a .NET coding add-in 123 | _ReSharper*/ 124 | *.[Rr]e[Ss]harper 125 | *.DotSettings.user 126 | 127 | # JustCode is a .NET coding add-in 128 | .JustCode 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # The packages folder can be ignored because of Package Restore 188 | **/[Pp]ackages/* 189 | # except build/, which is used as an MSBuild target. 190 | !**/[Pp]ackages/build/ 191 | # Uncomment if necessary however generally it will be regenerated when needed 192 | #!**/[Pp]ackages/repositories.config 193 | # NuGet v3's project.json files produces more ignorable files 194 | *.nuget.props 195 | *.nuget.targets 196 | 197 | # Microsoft Azure Build Output 198 | csx/ 199 | *.build.csdef 200 | 201 | # Microsoft Azure Emulator 202 | ecf/ 203 | rcf/ 204 | 205 | # Windows Store app package directories and files 206 | AppPackages/ 207 | BundleArtifacts/ 208 | Package.StoreAssociation.xml 209 | _pkginfo.txt 210 | *.appx 211 | 212 | # Visual Studio cache files 213 | # files ending in .cache can be ignored 214 | *.[Cc]ache 215 | # but keep track of directories ending in .cache 216 | !?*.[Cc]ache/ 217 | 218 | # Others 219 | ClientBin/ 220 | ~$* 221 | *~ 222 | *.dbmdl 223 | *.dbproj.schemaview 224 | *.jfm 225 | *.pfx 226 | *.publishsettings 227 | orleans.codegen.cs 228 | 229 | # Including strong name files can present a security risk 230 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 231 | #*.snk 232 | 233 | # Since there are multiple workflows, uncomment next line to ignore bower_components 234 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 235 | #bower_components/ 236 | 237 | # RIA/Silverlight projects 238 | Generated_Code/ 239 | 240 | # Backup & report files from converting an old project file 241 | # to a newer Visual Studio version. Backup files are not needed, 242 | # because we have git ;-) 243 | _UpgradeReport_Files/ 244 | Backup*/ 245 | UpgradeLog*.XML 246 | UpgradeLog*.htm 247 | ServiceFabricBackup/ 248 | *.rptproj.bak 249 | 250 | # SQL Server files 251 | *.mdf 252 | *.ldf 253 | *.ndf 254 | 255 | # Business Intelligence projects 256 | *.rdl.data 257 | *.bim.layout 258 | *.bim_*.settings 259 | *.rptproj.rsuser 260 | *- Backup*.rdl 261 | 262 | # Microsoft Fakes 263 | FakesAssemblies/ 264 | 265 | # GhostDoc plugin setting file 266 | *.GhostDoc.xml 267 | 268 | # Node.js Tools for Visual Studio 269 | .ntvs_analysis.dat 270 | node_modules/ 271 | 272 | # Visual Studio 6 build log 273 | *.plg 274 | 275 | # Visual Studio 6 workspace options file 276 | *.opt 277 | 278 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 279 | *.vbw 280 | 281 | # Visual Studio LightSwitch build output 282 | **/*.HTMLClient/GeneratedArtifacts 283 | **/*.DesktopClient/GeneratedArtifacts 284 | **/*.DesktopClient/ModelManifest.xml 285 | **/*.Server/GeneratedArtifacts 286 | **/*.Server/ModelManifest.xml 287 | _Pvt_Extensions 288 | 289 | # Paket dependency manager 290 | .paket/paket.exe 291 | paket-files/ 292 | 293 | # FAKE - F# Make 294 | .fake/ 295 | 296 | # JetBrains Rider 297 | .idea/ 298 | *.sln.iml 299 | 300 | # CodeRush personal settings 301 | .cr/personal 302 | 303 | # Python Tools for Visual Studio (PTVS) 304 | __pycache__/ 305 | *.pyc 306 | 307 | # Cake - Uncomment if you are using it 308 | # tools/** 309 | # !tools/packages.config 310 | 311 | # Tabs Studio 312 | *.tss 313 | 314 | # Telerik's JustMock configuration file 315 | *.jmconfig 316 | 317 | # BizTalk build output 318 | *.btp.cs 319 | *.btm.cs 320 | *.odx.cs 321 | *.xsd.cs 322 | 323 | # OpenCover UI analysis results 324 | OpenCover/ 325 | 326 | # Azure Stream Analytics local run output 327 | ASALocalRun/ 328 | 329 | # MSBuild Binary and Structured Log 330 | *.binlog 331 | 332 | # NVidia Nsight GPU debugger configuration file 333 | *.nvuser 334 | 335 | # MFractors (Xamarin productivity tool) working folder 336 | .mfractor/ 337 | 338 | # Local History for Visual Studio 339 | .localhistory/ 340 | 341 | # BeatPulse healthcheck temp database 342 | healthchecksdb 343 | 344 | Properties/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/OOXMLValidatorCLI/bin/Debug/netcoreapp3.1/OOXMLValidatorCLI.dll", 13 | "args": [ 14 | "C:\\source\\deleteThis\\add-extensions-to-validator\\Presentation1.pptm" 15 | ], 16 | "cwd": "${workspaceFolder}/OOXMLValidatorCLI", 17 | "console": "integratedTerminal", 18 | "stopAtEntry": false 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/OOXMLValidatorCLI/OOXMLValidatorCLI.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/OOXMLValidatorCLI/OOXMLValidatorCLI.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/OOXMLValidatorCLI/OOXMLValidatorCLI.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.1.5] - 2024-06-28 9 | 10 | ### Added 11 | - Updated NuGet packages 12 | - Updated Action to use setup-dotnet@4 13 | 14 | ## [2.1.4] - 2024-06-28 15 | 16 | ### Added 17 | - Add publish GitHub Action 18 | - Add test scripts for testing and publishing output 19 | - Automatically create release & assets 20 | 21 | ### Fixed 22 | - Fixes build for Linux (#21) 23 | 24 | ## [2.1.3] - 2024-01-25 25 | 26 | ### Fixed 27 | - Sets `RollForward` to `Major`, so extension will run on macos with later versions of dotnet 28 | 29 | ### Changed 30 | - Removed net7.0 build 31 | 32 | ## [2.1.2] - 2024-01-23 33 | 34 | ### Updated 35 | - Add build for dotnet 8 36 | 37 | ## [2.1.1] - 2024-01-22 38 | 39 | ### Updated 40 | 41 | - Updated to OOXML SDK 3.0 42 | - Updated to dotnet 8 43 | 44 | ## [2.1.1] - 2022-10-27 45 | 46 | ### Fixed 47 | 48 | - Update Path property on ValidationErrorInfoInternal to have type XmlPath 49 | 50 | ## [2.1.0] - 2022-09-29 51 | 52 | ### Added 53 | 54 | - Updated Open XML SDK Version 55 | 56 | ## [2.0.0] - 2022-07-27 57 | 58 | ### Added 59 | 60 | - Returned XML data returns list of `` elements with a child `` element, instead of list of `` elements. 61 | 62 | ### Fixed 63 | 64 | - If a file cannot be opened by the Validator, a `` element is added with the error message. 65 | 66 | ## [1.2.0] - 2022-04-28 67 | 68 | ### Added 69 | 70 | - Validation can be done on all OOXML files in a single directory or recursively through all child directories 71 | 72 | ## [1.1.0] - 2022-04-11 73 | 74 | ### Added 75 | 76 | - Validation errors can be returned as XML or JSON 77 | 78 | ## [1.0.0] - 2021-09-02 79 | 80 | ### Added 81 | 82 | - Validates OOXML files and returns JSON string of validation errors. 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Bowen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OOXMLValidator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34414.90 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OOXMLValidatorCLI", "OOXMLValidatorCLI\OOXMLValidatorCLI.csproj", "{992EF2AC-EDE5-4418-9584-E077999FD15E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OOXMLValidatorCLITests", "OOXMLValidatorCLITests\OOXMLValidatorCLITests.csproj", "{F5C0C69F-A8A2-4612-A2EE-3A15565C8FA8}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E8B33E39-2324-4289-9F3B-697CBBAC64B3}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | .gitignore = .gitignore 14 | CHANGELOG.md = CHANGELOG.md 15 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 16 | LICENSE = LICENSE 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {992EF2AC-EDE5-4418-9584-E077999FD15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {992EF2AC-EDE5-4418-9584-E077999FD15E}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {992EF2AC-EDE5-4418-9584-E077999FD15E}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {992EF2AC-EDE5-4418-9584-E077999FD15E}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {F5C0C69F-A8A2-4612-A2EE-3A15565C8FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {F5C0C69F-A8A2-4612-A2EE-3A15565C8FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {F5C0C69F-A8A2-4612-A2EE-3A15565C8FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {F5C0C69F-A8A2-4612-A2EE-3A15565C8FA8}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {A3BBF30A-BA8B-4409-B2B5-036990C1A424} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/DirectoryService.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using OOXMLValidatorCLI.Interfaces; 8 | 9 | /// 10 | /// Provides methods for working with directories and files. 11 | /// 12 | internal class DirectoryService : IDirectoryService 13 | { 14 | /// 15 | /// Enumerates files in a directory that match the specified search pattern and search option. 16 | /// 17 | /// The path to the directory. 18 | /// The search pattern to match against the file names. 19 | /// Specifies whether to search the current directory only or all subdirectories as well. 20 | /// An enumerable collection of file names that match the search pattern and search option. 21 | public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) 22 | { 23 | return Directory.EnumerateFiles(path, searchPattern, searchOption); 24 | } 25 | 26 | /// 27 | /// Returns the names of files in the specified directory. 28 | /// 29 | /// The path to the directory. 30 | /// An array of file names in the specified directory. 31 | public string[] GetFiles(string path) 32 | { 33 | return Directory.GetFiles(path); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/DocumentUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using DocumentFormat.OpenXml; 9 | using DocumentFormat.OpenXml.Packaging; 10 | using DocumentFormat.OpenXml.Validation; 11 | using OOXMLValidatorCLI.Interfaces; 12 | 13 | /// 14 | /// Utility class for working with Open XML documents. 15 | /// 16 | public class DocumentUtils : IDocumentUtils 17 | { 18 | /// 19 | /// Opens a WordprocessingDocument from the specified file path. 20 | /// 21 | /// The path of the Word document. 22 | /// The opened WordprocessingDocument. 23 | public WordprocessingDocument OpenWordprocessingDocument(string filePath) 24 | { 25 | return WordprocessingDocument.Open(filePath, false); 26 | } 27 | 28 | /// 29 | /// Opens a PresentationDocument from the specified file path. 30 | /// 31 | /// The path of the PowerPoint presentation. 32 | /// The opened PresentationDocument. 33 | public PresentationDocument OpenPresentationDocument(string filePath) 34 | { 35 | return PresentationDocument.Open(filePath, false); 36 | } 37 | 38 | /// 39 | /// Opens a SpreadsheetDocument from the specified file path. 40 | /// 41 | /// The path of the Excel spreadsheet. 42 | /// The opened SpreadsheetDocument. 43 | public SpreadsheetDocument OpenSpreadsheetDocument(string filePath) 44 | { 45 | return SpreadsheetDocument.Open(filePath, false); 46 | } 47 | 48 | /// 49 | /// Validates the specified OpenXmlPackage against the specified file format version. 50 | /// 51 | /// The OpenXmlPackage to validate. 52 | /// The file format version to validate against. 53 | /// A tuple containing a boolean indicating if the validation is strict, and a collection of validation error information. 54 | public Tuple> Validate(OpenXmlPackage doc, FileFormatVersions version) 55 | { 56 | OpenXmlValidator openXmlValidator = new OpenXmlValidator(version); 57 | bool isStrict = doc.StrictRelationshipFound; 58 | 59 | IEnumerable validationErrorInfos = openXmlValidator.Validate(doc); 60 | IEnumerable errors = validationErrorInfos.Select(e => new ValidationErrorInfoInternal() 61 | { 62 | ErrorType = Enum.GetName(e.ErrorType), 63 | Description = e.Description, 64 | Path = e.Path, 65 | Id = e.Id, 66 | }); 67 | 68 | return new Tuple>(isStrict, errors); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/FileService.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using System.IO; 6 | using OOXMLValidatorCLI.Interfaces; 7 | 8 | /// 9 | /// Represents a service for working with files. 10 | /// 11 | public class FileService : IFileService 12 | { 13 | /// 14 | /// Gets the attributes of a file at the specified path. 15 | /// 16 | /// The path of the file. 17 | /// The attributes of the file. 18 | public FileAttributes GetAttributes(string path) 19 | { 20 | return File.GetAttributes(path); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/FunctionUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Dynamic; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using DocumentFormat.OpenXml; 11 | using DocumentFormat.OpenXml.Packaging; 12 | using Newtonsoft.Json; 13 | using OOXMLValidatorCLI.Interfaces; 14 | 15 | /// 16 | /// Utility class for performing various functions related to Open XML documents. 17 | /// 18 | public class FunctionUtils : IFunctionUtils 19 | { 20 | private readonly IDocumentUtils documentUtils; 21 | private FileFormatVersions? fileFormatVersions; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The document utility object. 27 | public FunctionUtils(IDocumentUtils documentUtils) 28 | { 29 | this.documentUtils = documentUtils; 30 | this.fileFormatVersions = null; 31 | } 32 | 33 | /// 34 | /// Gets the maximum supported Office version based on the available FileFormatVersions. 35 | /// 36 | public FileFormatVersions OfficeVersion 37 | { 38 | get 39 | { 40 | return this.fileFormatVersions ?? Enum.GetValues(typeof(FileFormatVersions)).Cast().Max(); 41 | } 42 | } 43 | 44 | /// 45 | /// Gets the OpenXmlPackage object for the specified file. 46 | /// 47 | /// The path of the file. 48 | /// The extension of the file. 49 | /// The OpenXmlPackage object. 50 | public OpenXmlPackage GetDocument(string filePath, string fileExtension) 51 | { 52 | OpenXmlPackage doc = null; 53 | 54 | switch (fileExtension) 55 | { 56 | case ".docx": 57 | case ".docm": 58 | case ".dotm": 59 | case ".dotx": 60 | doc = this.documentUtils.OpenWordprocessingDocument(filePath); 61 | break; 62 | case ".pptx": 63 | case ".pptm": 64 | case ".potm": 65 | case ".potx": 66 | case ".ppam": 67 | case ".ppsm": 68 | case ".ppsx": 69 | doc = this.documentUtils.OpenPresentationDocument(filePath); 70 | break; 71 | case ".xlsx": 72 | case ".xlsm": 73 | case ".xltm": 74 | case ".xltx": 75 | case ".xlam": 76 | doc = this.documentUtils.OpenSpreadsheetDocument(filePath); 77 | break; 78 | default: 79 | break; 80 | } 81 | 82 | return doc; 83 | } 84 | 85 | /// 86 | /// Sets the Office version based on the provided string value. 87 | /// 88 | /// The string representation of the Office version. 89 | public void SetOfficeVersion(string v) 90 | { 91 | if (v is not null && Enum.TryParse(v, out FileFormatVersions version)) 92 | { 93 | this.fileFormatVersions = version; 94 | } 95 | else 96 | { 97 | FileFormatVersions currentVersion = Enum.GetValues(typeof(FileFormatVersions)).Cast().Last(); 98 | this.fileFormatVersions = currentVersion; 99 | } 100 | } 101 | 102 | /// 103 | /// Validates the specified OpenXmlPackage object and returns the validation errors. 104 | /// 105 | /// The OpenXmlPackage object to validate. 106 | /// A tuple containing a boolean value indicating if the validation is strict and a collection of validation error information. 107 | public Tuple> GetValidationErrors(OpenXmlPackage doc) 108 | { 109 | return this.documentUtils.Validate(doc, this.OfficeVersion); 110 | } 111 | 112 | /// 113 | /// Gets the validation errors data in the specified format. 114 | /// 115 | /// The validation information. 116 | /// The path of the file. 117 | /// A boolean value indicating if the data should be returned in XML format. 118 | /// The validation errors data. 119 | public object GetValidationErrorsData(Tuple> validationInfo, string filePath, bool returnXml) 120 | { 121 | if (!returnXml) 122 | { 123 | List res = new List(); 124 | 125 | foreach (ValidationErrorInfoInternal validationErrorInfo in validationInfo.Item2) 126 | { 127 | dynamic dyno = new ExpandoObject(); 128 | dyno.Description = validationErrorInfo.Description; 129 | dyno.Path = validationErrorInfo.Path; 130 | dyno.Id = validationErrorInfo.Id; 131 | dyno.ErrorType = validationErrorInfo.ErrorType; 132 | res.Add(dyno); 133 | } 134 | 135 | string json = JsonConvert.SerializeObject( 136 | res, 137 | Formatting.None, 138 | new JsonSerializerSettings() 139 | { 140 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 141 | }); 142 | 143 | return json; 144 | } 145 | else 146 | { 147 | XElement element; 148 | ValidationErrorInfoInternal first = validationInfo.Item2.FirstOrDefault(); 149 | 150 | if (first?.ErrorType == "OpenXmlPackageException") 151 | { 152 | element = new XElement( 153 | "Exceptions", 154 | new XElement( 155 | "OpenXmlPackageException", 156 | new XElement("Message", first.Description))); 157 | } 158 | else 159 | { 160 | element = new XElement("ValidationErrorInfoList"); 161 | 162 | foreach (ValidationErrorInfoInternal validationErrorInfo in validationInfo.Item2) 163 | { 164 | element.Add( 165 | new XElement( 166 | "ValidationErrorInfo", 167 | new XElement("Description", validationErrorInfo.Description), 168 | new XElement("Path", validationErrorInfo.Path), 169 | new XElement("Id", validationErrorInfo.Id), 170 | new XElement("ErrorType", validationErrorInfo.ErrorType))); 171 | } 172 | } 173 | 174 | XElement xml = new XElement("File", element); 175 | xml.SetAttributeValue("FilePath", filePath); 176 | xml.SetAttributeValue("IsStrict", validationInfo.Item1); 177 | 178 | return new XDocument(xml); 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/Validate.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using DocumentFormat.OpenXml.Packaging; 11 | using Newtonsoft.Json; 12 | using OOXMLValidatorCLI.Interfaces; 13 | 14 | /// 15 | /// Represents a class that provides validation functionality for OOXML files. 16 | /// 17 | public class Validate : IValidate 18 | { 19 | private readonly IFunctionUtils functionUtils; 20 | private readonly string[] validFileExtensions = new string[] { ".docx", ".docm", ".dotm", ".dotx", ".pptx", ".pptm", ".potm", ".potx", ".ppam", ".ppsm", ".ppsx", ".xlsx", ".xlsm", ".xltm", ".xltx", ".xlam" }; 21 | private readonly IFileService fileService; 22 | private readonly IDirectoryService directoryService; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The function utilities. 28 | /// The file service. 29 | /// The directory service. 30 | public Validate(IFunctionUtils functionUtils, IFileService fileService, IDirectoryService directoryService) 31 | { 32 | this.functionUtils = functionUtils; 33 | this.fileService = fileService; 34 | this.directoryService = directoryService; 35 | } 36 | 37 | /// 38 | /// Validates the specified OOXML file. 39 | /// 40 | /// The path to the file. 41 | /// The format of the file. 42 | /// Indicates whether to return the validation errors as XML. 43 | /// Indicates whether to recursively validate files in subdirectories. 44 | /// Indicates whether to include valid files in the result. 45 | /// The validation result. 46 | public object OOXML(string filePath, string format, bool returnXml = false, bool recursive = false, bool includeValid = false) 47 | { 48 | this.functionUtils.SetOfficeVersion(format); 49 | 50 | FileAttributes fileAttributes = this.fileService.GetAttributes(filePath); 51 | 52 | if (fileAttributes.HasFlag(FileAttributes.Directory)) 53 | { 54 | IEnumerable files = recursive ? this.directoryService.EnumerateFiles(filePath, "*.*", SearchOption.AllDirectories).Where(f => this.validFileExtensions.Contains(Path.GetExtension(f))) 55 | : this.directoryService.GetFiles(filePath).Where(f => this.validFileExtensions.Contains(Path.GetExtension(f))); 56 | 57 | XDocument xDocument = new XDocument(new XElement("Document")); 58 | List validationErrorList = new List(); 59 | 60 | foreach (string file in files) 61 | { 62 | string fileExtension = Path.GetExtension(file); 63 | 64 | Tuple> validationTuple = this.GetValidationErrors(file, fileExtension); 65 | 66 | if (validationTuple.Item2.Count() > 0 || includeValid) 67 | { 68 | var data = this.functionUtils.GetValidationErrorsData(validationTuple, file, returnXml); 69 | 70 | if (returnXml) 71 | { 72 | xDocument.Root.Add((data as XDocument).Root); 73 | } 74 | else 75 | { 76 | var errorList = new { FilePath = file, ValidationErrors = data }; 77 | 78 | validationErrorList.Add(errorList); 79 | } 80 | } 81 | } 82 | 83 | return returnXml ? xDocument : JsonConvert.SerializeObject( 84 | validationErrorList, 85 | Formatting.None, 86 | new JsonSerializerSettings() 87 | { 88 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 89 | }); 90 | } 91 | else 92 | { 93 | string fileExtension = Path.GetExtension(filePath); 94 | 95 | if (!this.validFileExtensions.Contains(fileExtension)) 96 | { 97 | throw new ArgumentException(string.Concat("file must have one of these extensions: ", string.Join(", ", this.validFileExtensions))); 98 | } 99 | 100 | Tuple> validationErrorInfos = this.GetValidationErrors(filePath, fileExtension); 101 | 102 | return this.functionUtils.GetValidationErrorsData(validationErrorInfos, filePath, returnXml); 103 | } 104 | } 105 | 106 | /// 107 | /// Gets the validation errors for the specified file. 108 | /// 109 | /// The path to the file. 110 | /// The extension of the file. 111 | /// A tuple containing a boolean indicating if the file is valid and a collection of validation error information. 112 | private Tuple> GetValidationErrors(string filePath, string fileExtension) 113 | { 114 | try 115 | { 116 | OpenXmlPackage doc = this.functionUtils.GetDocument(filePath, fileExtension); 117 | 118 | return this.functionUtils.GetValidationErrors(doc); 119 | } 120 | catch (Exception ex) 121 | { 122 | List errors = new List { new ValidationErrorInfoInternal() { Description = ex.Message, ErrorType = "OpenXmlPackageException" } }; 123 | 124 | return new Tuple>(false, errors); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Classes/ValidationErrorInfoInternal.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Classes 4 | { 5 | using DocumentFormat.OpenXml; 6 | 7 | /// 8 | /// Represents information about a validation error. 9 | /// 10 | public class ValidationErrorInfoInternal 11 | { 12 | /// 13 | /// Gets or sets the type of the error. 14 | /// 15 | public string ErrorType { get; set; } 16 | 17 | /// 18 | /// Gets or sets the description of the error. 19 | /// 20 | public string Description { get; set; } 21 | 22 | /// 23 | /// Gets or sets the XML path of the error. 24 | /// 25 | public XmlPath Path { get; set; } 26 | 27 | /// 28 | /// Gets or sets the ID of the error. 29 | /// 30 | public string Id { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Interfaces/IDirectoryService.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Interfaces 4 | { 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | /// 9 | /// Represents a directory service for working with directories and files. 10 | /// 11 | public interface IDirectoryService 12 | { 13 | /// 14 | /// Enumerates files in a specified directory that match a specified search pattern. 15 | /// 16 | /// The path to the directory to search. 17 | /// The search string to match against the names of files in the directory. 18 | /// Specifies whether to search the current directory, or all subdirectories as well. 19 | /// An enumerable collection of the full names (including paths) for the files in the directory that match the specified search pattern. 20 | IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption); 21 | 22 | /// 23 | /// Returns the names of files in a specified directory. 24 | /// 25 | /// The path to the directory to search. 26 | /// An array of the full names (including paths) for the files in the directory. 27 | string[] GetFiles(string path); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Interfaces/IDocumentUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Interfaces 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using DocumentFormat.OpenXml; 8 | using DocumentFormat.OpenXml.Packaging; 9 | using OOXMLValidatorCLI.Classes; 10 | 11 | /// 12 | /// Represents a utility for working with Open XML documents. 13 | /// 14 | public interface IDocumentUtils 15 | { 16 | /// 17 | /// Opens a WordprocessingDocument from the specified temporary file path. 18 | /// 19 | /// The temporary file path. 20 | /// The opened WordprocessingDocument. 21 | WordprocessingDocument OpenWordprocessingDocument(string tempFilePath); 22 | 23 | /// 24 | /// Opens a SpreadsheetDocument from the specified temporary file path. 25 | /// 26 | /// The temporary file path. 27 | /// The opened SpreadsheetDocument. 28 | SpreadsheetDocument OpenSpreadsheetDocument(string tempFilePath); 29 | 30 | /// 31 | /// Opens a PresentationDocument from the specified temporary file path. 32 | /// 33 | /// The temporary file path. 34 | /// The opened PresentationDocument. 35 | PresentationDocument OpenPresentationDocument(string tempFilePath); 36 | 37 | /// 38 | /// Validates the specified OpenXmlPackage against the specified file format versions. 39 | /// 40 | /// The OpenXmlPackage to validate. 41 | /// The file format versions to validate against. 42 | /// A tuple containing a boolean indicating whether the validation succeeded and a collection of validation error information. 43 | Tuple> Validate(OpenXmlPackage doc, FileFormatVersions version); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Interfaces/IFileService.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Interfaces 4 | { 5 | using System.IO; 6 | 7 | /// 8 | /// Represents a file service that provides operations related to file attributes. 9 | /// 10 | public interface IFileService 11 | { 12 | /// 13 | /// Gets the attributes of the specified file. 14 | /// 15 | /// The path to the file. 16 | /// The attributes of the file. 17 | FileAttributes GetAttributes(string path); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Interfaces/IFunctionUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Interfaces 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using DocumentFormat.OpenXml; 8 | using DocumentFormat.OpenXml.Packaging; 9 | using OOXMLValidatorCLI.Classes; 10 | 11 | /// 12 | /// Represents a set of utility functions for working with Open XML documents. 13 | /// 14 | public interface IFunctionUtils 15 | { 16 | /// 17 | /// Gets the Office version for the Open XML document. 18 | /// 19 | FileFormatVersions OfficeVersion { get; } 20 | 21 | /// 22 | /// Sets the Office version for the Open XML document. 23 | /// 24 | /// The Office version to set. 25 | void SetOfficeVersion(string version); 26 | 27 | /// 28 | /// Gets the OpenXmlPackage object for the specified file. 29 | /// 30 | /// The path of the file. 31 | /// The extension of the file. 32 | /// The OpenXmlPackage object. 33 | OpenXmlPackage GetDocument(string filePath, string fileExtension); 34 | 35 | /// 36 | /// Gets the validation errors for the specified OpenXmlPackage object. 37 | /// 38 | /// The OpenXmlPackage object. 39 | /// A tuple containing a boolean value indicating whether there are validation errors and a collection of validation error information. 40 | Tuple> GetValidationErrors(OpenXmlPackage doc); 41 | 42 | /// 43 | /// Gets the validation errors data in the specified format. 44 | /// 45 | /// The tuple containing the validation errors information. 46 | /// The path of the file. 47 | /// A boolean value indicating whether to return the validation errors data as XML. 48 | /// The validation errors data. 49 | object GetValidationErrorsData(Tuple> data, string filePath, bool returnXml); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Interfaces/IValidate.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLI.Interfaces 4 | { 5 | /// 6 | /// Represents an interface for validating OOXML files. 7 | /// 8 | public interface IValidate 9 | { 10 | /// 11 | /// Validates the specified OOXML file. 12 | /// 13 | /// The path to the OOXML file. 14 | /// The format of the validation result. 15 | /// Specifies whether to return the validation result as XML. 16 | /// Specifies whether to validate files recursively in subdirectories. 17 | /// Specifies whether to include valid files in the validation result. 18 | /// The validation result. 19 | object OOXML(string filePath, string format, bool returnXml, bool recursive, bool includeValid); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/OOXMLValidatorCLI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | Major 7 | true 8 | README.md 9 | https://github.com/mikeebowen/OOXML-Validator 10 | git 11 | 2.1.3 12 | true 13 | true 14 | 15 | 16 | 17 | win-x64;linux-x64;osx-x64 18 | true 19 | true 20 | true 21 | true 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | \ 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | all 48 | runtime; build; native; contentfiles; analyzers; buildtransitive 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | 60 | 61 | 62 | ResXFileCodeGenerator 63 | Resources.Designer.cs 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/Program.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | OOXMLValidatorCLI.Program.Start(args); 4 | 5 | namespace OOXMLValidatorCLI 6 | { 7 | using System; 8 | using DocumentFormat.OpenXml; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using OOXMLValidatorCLI.Classes; 11 | using OOXMLValidatorCLI.Interfaces; 12 | 13 | /// 14 | /// Represents the program's entry point. 15 | /// 16 | public class Program 17 | { 18 | /// 19 | /// Entry point of the application. 20 | /// 21 | /// Command line arguments. 22 | public static void Start(string[] args) 23 | { 24 | try 25 | { 26 | // set up DI 27 | ServiceCollection collection = new ServiceCollection(); 28 | collection.AddScoped(); 29 | collection.AddScoped(); 30 | collection.AddScoped(); 31 | collection.AddSingleton(); 32 | collection.AddSingleton(); 33 | 34 | ServiceProvider serviceProvider = collection.BuildServiceProvider(); 35 | 36 | IValidate validate = serviceProvider.GetService(); 37 | 38 | string xmlPath; 39 | bool returnXml = false; 40 | string version = null; 41 | bool recursive = false; 42 | bool includeValid = false; 43 | 44 | if (args is not null && args.Length > 0) 45 | { 46 | xmlPath = args[0]; 47 | 48 | for (int i = 1; i < args.Length; i++) 49 | { 50 | if (Enum.TryParse(args[i], out FileFormatVersions v)) 51 | { 52 | version = args[i]; 53 | } 54 | else 55 | { 56 | switch (args[i]) 57 | { 58 | case "--xml": 59 | returnXml = true; 60 | break; 61 | case "-x": 62 | returnXml = true; 63 | break; 64 | case "--recursive": 65 | recursive = true; 66 | break; 67 | case "-r": 68 | recursive = true; 69 | break; 70 | case "--all": 71 | includeValid = true; 72 | break; 73 | case "-a": 74 | includeValid = true; 75 | break; 76 | default: throw new ArgumentException("Unknown argument", args[i]); 77 | } 78 | } 79 | } 80 | } 81 | else 82 | { 83 | throw new ArgumentNullException(); 84 | } 85 | 86 | object validationErrors = validate.OOXML(xmlPath, version, returnXml, recursive, includeValid); 87 | 88 | Console.Write(validationErrors); 89 | } 90 | catch (Exception ex) 91 | { 92 | Console.WriteLine(ex.Message); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /OOXMLValidatorCLI/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Michael Bowen", 12 | "xmlHeader": false, 13 | "copyrightText": "Licensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", 14 | "variables": { 15 | "licenseName": "MIT", 16 | "licenseFile": "LICENSE" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OOXMLValidatorCLITests/FunctionUtilsTests.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLITests 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using DocumentFormat.OpenXml; 11 | using DocumentFormat.OpenXml.Packaging; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | using Newtonsoft.Json; 15 | using OOXMLValidatorCLI.Classes; 16 | using OOXMLValidatorCLI.Interfaces; 17 | 18 | /// 19 | /// Unit tests for the FunctionUtils class. 20 | /// 21 | [TestClass] 22 | public class FunctionUtilsTests 23 | { 24 | /// 25 | /// Test case to verify that the OfficeVersion property is set correctly. 26 | /// 27 | [TestMethod] 28 | public void ShouldSetOfficeVersion() 29 | { 30 | // Arrange 31 | var documentMock = Mock.Of(); 32 | FunctionUtils functionUtils = new FunctionUtils(documentMock); 33 | 34 | // Act 35 | var officeVersion = functionUtils.OfficeVersion; 36 | 37 | // Assert 38 | Assert.AreEqual(officeVersion, Enum.GetValues(typeof(FileFormatVersions)).Cast().Max()); 39 | } 40 | 41 | /// 42 | /// Test case to verify that the GetDocument method calls the correct Open method for Word documents. 43 | /// 44 | [TestMethod] 45 | public void GetDocument_ShouldCallCorrectOpenMethodWord() 46 | { 47 | // Arrange 48 | string testPath = "foo/bar/baz.docx"; 49 | var documentUtilsMock = Mock.Of(); 50 | MemoryStream memoryStream = new MemoryStream(); 51 | 52 | memoryStream.Seek(0, SeekOrigin.Begin); 53 | 54 | using WordprocessingDocument testDocument = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 55 | 56 | Mock.Get(documentUtilsMock) 57 | .Setup(d => d.OpenWordprocessingDocument(It.IsAny())) 58 | .Callback(s => 59 | { 60 | Assert.AreEqual(testPath, s); 61 | }) 62 | .Returns(testDocument); 63 | FunctionUtils functionUtils = new FunctionUtils(documentUtilsMock); 64 | 65 | // Act 66 | var res = functionUtils.GetDocument(testPath, ".docx"); 67 | 68 | // Assert 69 | Assert.AreEqual(res, testDocument); 70 | 71 | Mock.Get(documentUtilsMock).Verify(f => f.OpenWordprocessingDocument(testPath), Times.Once); 72 | } 73 | 74 | /// 75 | /// Test case to verify that the GetDocument method calls the correct Open method for Presentation documents. 76 | /// 77 | [TestMethod] 78 | public void GetDocument_ShouldCallCorrectOpenMethodPresentation() 79 | { 80 | // Arrange 81 | string testPath = "foo/bar/baz.pptx"; 82 | var documentMock = Mock.Of(); 83 | MemoryStream memoryStream = new MemoryStream(); 84 | 85 | memoryStream.Seek(0, SeekOrigin.Begin); 86 | 87 | using PresentationDocument testDocument = PresentationDocument.Create(memoryStream, PresentationDocumentType.Presentation); 88 | 89 | Mock.Get(documentMock) 90 | .Setup(d => d.OpenPresentationDocument(It.IsAny())) 91 | .Callback(s => 92 | { 93 | Assert.AreEqual(testPath, s); 94 | }) 95 | .Returns(testDocument); 96 | FunctionUtils functionUtils = new FunctionUtils(documentMock); 97 | 98 | // Act 99 | var res = functionUtils.GetDocument(testPath, ".pptx"); 100 | 101 | // Assert 102 | Assert.AreEqual(res, testDocument); 103 | 104 | Mock.Get(documentMock).Verify(f => f.OpenPresentationDocument(testPath), Times.Once); 105 | } 106 | 107 | /// 108 | /// Test case to verify that the GetDocument method calls the correct Open method for Spreadsheet documents. 109 | /// 110 | [TestMethod] 111 | public void GetDocument_ShouldCallCorrectOpenMethodSpreadsheet() 112 | { 113 | // Arrange 114 | string testPath = "foo/bar/baz.xlsx"; 115 | var documentMock = Mock.Of(); 116 | MemoryStream memoryStream = new MemoryStream(); 117 | using SpreadsheetDocument testDynamic = SpreadsheetDocument.Create(memoryStream, SpreadsheetDocumentType.Workbook); 118 | 119 | memoryStream.Seek(0, SeekOrigin.Begin); 120 | 121 | Mock.Get(documentMock) 122 | .Setup(d => d.OpenSpreadsheetDocument(It.IsAny())) 123 | .Callback(s => 124 | { 125 | Assert.AreEqual(testPath, s); 126 | }) 127 | .Returns(testDynamic); 128 | FunctionUtils functionUtils = new FunctionUtils(documentMock); 129 | 130 | // Act 131 | var res = functionUtils.GetDocument(testPath, ".xlsx"); 132 | 133 | // Assert 134 | Assert.AreEqual(res, testDynamic); 135 | 136 | Mock.Get(documentMock).Verify(f => f.OpenSpreadsheetDocument(testPath), Times.Once); 137 | } 138 | 139 | /// 140 | /// Test case to verify that the SetOfficeVersion method sets the valid version. 141 | /// 142 | [TestMethod] 143 | public void SetOfficeVersion_ShouldSetValidVersion() 144 | { 145 | // Arrange 146 | var documentMock = Mock.Of(); 147 | FunctionUtils functionUtils = new FunctionUtils(documentMock); 148 | 149 | // Act 150 | functionUtils.SetOfficeVersion("Office2016"); 151 | 152 | // Assert 153 | Assert.AreEqual(functionUtils.OfficeVersion, FileFormatVersions.Office2016); 154 | } 155 | 156 | /// 157 | /// Test case to verify that the SetOfficeVersion method sets the default version. 158 | /// 159 | [TestMethod] 160 | public void SetOfficeVersion_ShouldSetDefaultVersion() 161 | { 162 | // Arrange 163 | var documentMock = Mock.Of(); 164 | FunctionUtils functionUtils = new FunctionUtils(documentMock); 165 | 166 | // Act 167 | functionUtils.SetOfficeVersion(null); 168 | 169 | // Assert 170 | Assert.AreEqual(functionUtils.OfficeVersion, Enum.GetValues(typeof(FileFormatVersions)).Cast().Last()); 171 | } 172 | 173 | /// 174 | /// Test case to verify that the GetValidationErrorsData method returns valid JSON. 175 | /// 176 | [TestMethod] 177 | public void GetValidationErrorsData_ShouldReturnValidJson() 178 | { 179 | // Arrange 180 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 181 | var documentMock = Mock.Of(); 182 | string testJson = "\"[{\\\"Description\\\":null,\\\"Path\\\":null,\\\"Id\\\":null,\\\"ErrorType\\\":null},{\\\"Description\\\":null,\\\"Path\\\":null,\\\"Id\\\":null,\\\"ErrorType\\\":null},{\\\"Description\\\":null,\\\"Path\\\":null,\\\"Id\\\":null,\\\"ErrorType\\\":null}]\""; 183 | 184 | var functionUtils = new FunctionUtils(documentMock); 185 | 186 | // Act 187 | object res = functionUtils.GetValidationErrorsData(Tuple.Create(true, validationErrorInfos), @"C:\test\file\path.xlsx", false); 188 | string jsonData = JsonConvert.SerializeObject(res); 189 | 190 | // Assert 191 | Assert.AreEqual(jsonData, testJson); 192 | } 193 | 194 | /// 195 | /// Test case to verify that the GetValidationErrorsData method returns valid XML. 196 | /// 197 | [TestMethod] 198 | public void GetValidationErrorsData_ShouldReturnValidXml() 199 | { 200 | // Arrange 201 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 202 | var documentMock = Mock.Of(); 203 | string xmlString = ""; 204 | XDocument xDoc = XDocument.Parse(xmlString); 205 | 206 | var functionUtils = new FunctionUtils(documentMock); 207 | 208 | // Act 209 | object res = functionUtils.GetValidationErrorsData(Tuple.Create(true, validationErrorInfos), @"C:\test\file\path.xlsx", true); 210 | 211 | // Assert 212 | Assert.IsTrue(XNode.DeepEquals(res as XDocument, xDoc)); 213 | } 214 | 215 | /// 216 | /// Test case to verify that the GetValidationErrors method calls the Validate method. 217 | /// 218 | [TestMethod] 219 | public void GetValidationErrors_ShouldCallValidate() 220 | { 221 | // Arrange 222 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 223 | var testTup = Tuple.Create(true, validationErrorInfos); 224 | var documentMock = Mock.Of(); 225 | 226 | MemoryStream memoryStream = new MemoryStream(); 227 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 228 | 229 | Mock.Get(documentMock).Setup(d => d.Validate(It.IsAny(), It.IsAny())).Returns(testTup); 230 | 231 | var functionUtils = new FunctionUtils(documentMock); 232 | 233 | // Act 234 | var resTup = functionUtils.GetValidationErrors(testWordDoc); 235 | 236 | // Assert 237 | Assert.AreEqual(testTup, resTup); 238 | Mock.Get(documentMock).Verify(d => d.Validate(It.IsAny(), It.IsAny()), Times.Once()); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /OOXMLValidatorCLITests/OOXMLValidatorCLITests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /OOXMLValidatorCLITests/ValidateTests.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace OOXMLValidatorCLITests 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Xml.Linq; 9 | using DocumentFormat.OpenXml; 10 | using DocumentFormat.OpenXml.Packaging; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | using Moq; 13 | using OOXMLValidatorCLI.Classes; 14 | using OOXMLValidatorCLI.Interfaces; 15 | 16 | /// 17 | /// Contains unit tests for the Validate class. 18 | /// 19 | [TestClass] 20 | public class ValidateTests 21 | { 22 | /// 23 | /// Validates a single file. 24 | /// 25 | [TestMethod] 26 | public void Validate_ShouldValidateASingleFile() 27 | { 28 | // Arrange 29 | var functionUtilsMock = Mock.Of(); 30 | var fileServiceMock = Mock.Of(); 31 | var directoryServiceMock = Mock.Of(); 32 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 33 | string testPath = "path/to/a/file.docx"; 34 | string testFormat = "Office2016"; 35 | MemoryStream memoryStream = new MemoryStream(); 36 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 37 | 38 | memoryStream.Seek(0, SeekOrigin.Begin); 39 | 40 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 41 | 42 | Mock.Get(functionUtilsMock).Setup(f => f.GetDocument(It.IsAny(), ".docx")).Returns(testWordDoc); 43 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrors(It.IsAny())) 44 | .Callback(o => 45 | { 46 | Assert.AreEqual(testWordDoc, o); 47 | }) 48 | .Returns(new Tuple>(true, validationErrorInfos)); 49 | 50 | // Act 51 | validate.OOXML(testPath, testFormat); 52 | 53 | // Assert 54 | Mock.Get(functionUtilsMock).Verify(f => f.SetOfficeVersion(testFormat), Times.Once()); 55 | Mock.Get(functionUtilsMock).Verify(f => f.GetDocument(testPath, ".docx"), Times.Once()); 56 | Mock.Get(functionUtilsMock).Verify(f => f.GetValidationErrorsData(new Tuple>(true, validationErrorInfos), testPath, false), Times.Once()); 57 | } 58 | 59 | /// 60 | /// Validates that an exception is thrown with an invalid file type. 61 | /// 62 | [TestMethod] 63 | [ExpectedException(typeof(ArgumentException), "file must be have one of these extensions: .docx, .docm, .dotm, .dotx, .pptx, .pptm, .potm, .potx, .ppam, .ppsm, .ppsx, .xlsx, .xlsm, .xltm, .xltx, .xlam")] 64 | public void Validate_ShouldThrowAnExceptionWithInvalidFileType() 65 | { 66 | // Arrange 67 | var functionUtilsMock = Mock.Of(); 68 | var fileServiceMock = Mock.Of(); 69 | var directoryServiceMock = Mock.Of(); 70 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 71 | string testPath = "path/to/a/file.foo"; 72 | string testFormat = "Office2016"; 73 | 74 | // Act and Assert 75 | validate.OOXML(testPath, testFormat); 76 | Mock.Get(fileServiceMock).Setup(f => f.GetAttributes(testPath)).Returns(FileAttributes.Normal); 77 | } 78 | 79 | /// 80 | /// Validates a folder and returns the result in JSON format. 81 | /// 82 | [TestMethod] 83 | public void Validate_ShouldValidateAFolderAndReturnJson() 84 | { 85 | // Arrange 86 | var functionUtilsMock = Mock.Of(); 87 | var fileServiceMock = Mock.Of(); 88 | var directoryServiceMock = Mock.Of(); 89 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 90 | string testPath = "path/to/files/"; 91 | string testFormat = null; 92 | MemoryStream memoryStream = new MemoryStream(); 93 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 94 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 95 | 96 | memoryStream.Seek(0, SeekOrigin.Begin); 97 | 98 | Mock.Get(fileServiceMock).Setup(f => f.GetAttributes(testPath)).Returns(FileAttributes.Directory); 99 | Mock.Get(directoryServiceMock).Setup(d => d.GetFiles(testPath)).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 100 | Mock.Get(functionUtilsMock).Setup(f => f.GetDocument(It.IsAny(), It.IsAny())).Returns(testWordDoc); 101 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrors(It.IsAny())).Returns(new Tuple>(false, validationErrorInfos)); 102 | 103 | // Act 104 | object validationErrors = validate.OOXML(testPath, testFormat, false, false); 105 | 106 | // Assert 107 | Assert.IsNotNull(validationErrors); 108 | Assert.AreEqual(validationErrors, "[{\"FilePath\":\"taco.docx\",\"ValidationErrors\":null},{\"FilePath\":\"cat.pptx\",\"ValidationErrors\":null},{\"FilePath\":\"foo.xlsx\",\"ValidationErrors\":null},{\"FilePath\":\"bar.docm\",\"ValidationErrors\":null}]"); 109 | } 110 | 111 | /// 112 | /// Validates a folder and returns the result in XML format. 113 | /// 114 | [TestMethod] 115 | public void Validate_ShouldValidateAFolderAndReturnXml() 116 | { 117 | // Arrange 118 | var functionUtilsMock = Mock.Of(); 119 | var fileServiceMock = Mock.Of(); 120 | var directoryServiceMock = Mock.Of(); 121 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 122 | string testPath = "path/to/files/"; 123 | string testFormat = null; 124 | MemoryStream memoryStream = new MemoryStream(); 125 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 126 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 127 | memoryStream.Seek(0, SeekOrigin.Begin); 128 | 129 | XDocument testXDocument = new XDocument(new XElement("Document")); 130 | XElement testXml = new XElement("KittyErrorInfoList"); 131 | testXml.Add( 132 | new XElement( 133 | "KittyErrorInfo", 134 | new XElement("Description", "The cat peed on the carpet"), 135 | new XElement("Path", "office/carpet"), 136 | new XElement("Id", "305"), 137 | new XElement("ErrorType", "KittyPee"))); 138 | 139 | for (int i = 0; i < 4; i++) 140 | { 141 | testXDocument.Root.Add(XElement.Parse(testXml.ToString())); 142 | } 143 | 144 | Mock.Get(fileServiceMock).Setup(f => f.GetAttributes(testPath)).Returns(FileAttributes.Directory); 145 | Mock.Get(directoryServiceMock).Setup(d => d.GetFiles(testPath)).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 146 | Mock.Get(functionUtilsMock).Setup(f => f.GetDocument(It.IsAny(), It.IsAny())).Returns(testWordDoc); 147 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrors(It.IsAny())).Returns(new Tuple>(false, validationErrorInfos)); 148 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrorsData(It.IsAny>>(), It.IsAny(), It.IsAny())).Returns(new XDocument(testXml)); 149 | 150 | // Act 151 | object validationErrorXml = validate.OOXML(testPath, testFormat, true, false); 152 | 153 | // Assert 154 | Assert.IsNotNull(validationErrorXml); 155 | Assert.AreEqual(validationErrorXml.ToString(), testXDocument.ToString()); 156 | } 157 | 158 | /// 159 | /// Validates a folder recursively with the flag and returns the result. 160 | /// 161 | [TestMethod] 162 | public void Validate_ShouldTestRecursivelyWithFlag() 163 | { 164 | // Arrange 165 | var functionUtilsMock = Mock.Of(); 166 | var fileServiceMock = Mock.Of(); 167 | var directoryServiceMock = Mock.Of(); 168 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 169 | string testPath = "path/to/files/"; 170 | string testFormat = null; 171 | MemoryStream memoryStream = new MemoryStream(); 172 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 173 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 174 | 175 | memoryStream.Seek(0, SeekOrigin.Begin); 176 | 177 | Mock.Get(fileServiceMock).Setup(f => f.GetAttributes(testPath)).Returns(FileAttributes.Directory); 178 | Mock.Get(directoryServiceMock).Setup(d => d.GetFiles(testPath)).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 179 | Mock.Get(directoryServiceMock).Setup(d => d.EnumerateFiles(It.IsAny(), It.IsAny(), It.IsAny())).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 180 | Mock.Get(functionUtilsMock).Setup(f => f.GetDocument(It.IsAny(), It.IsAny())).Returns(testWordDoc); 181 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrors(It.IsAny())).Returns(new Tuple>(false, validationErrorInfos)); 182 | 183 | // Act 184 | object validationErrors = validate.OOXML(testPath, testFormat, false, true); 185 | 186 | // Assert 187 | Assert.IsNotNull(validationErrors); 188 | Assert.AreEqual(validationErrors, "[{\"FilePath\":\"taco.docx\",\"ValidationErrors\":null},{\"FilePath\":\"cat.pptx\",\"ValidationErrors\":null},{\"FilePath\":\"foo.xlsx\",\"ValidationErrors\":null},{\"FilePath\":\"bar.docm\",\"ValidationErrors\":null}]"); 189 | Mock.Get(directoryServiceMock).Verify(d => d.EnumerateFiles(testPath, "*.*", SearchOption.AllDirectories), Times.Once); 190 | Mock.Get(directoryServiceMock).Verify(d => d.GetFiles(testPath), Times.Never); 191 | } 192 | 193 | /// 194 | /// Validates files without testing them recursively. 195 | /// 196 | [TestMethod] 197 | public void Validate_ShouldNotTestFilesRecursivelyWithoutFlag() 198 | { 199 | // Arrange 200 | var functionUtilsMock = Mock.Of(); 201 | var fileServiceMock = Mock.Of(); 202 | var directoryServiceMock = Mock.Of(); 203 | var validate = new Validate(functionUtilsMock, fileServiceMock, directoryServiceMock); 204 | string testPath = "path/to/files/"; 205 | string testFormat = null; 206 | MemoryStream memoryStream = new MemoryStream(); 207 | using WordprocessingDocument testWordDoc = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document); 208 | IEnumerable validationErrorInfos = new List() { new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal(), new ValidationErrorInfoInternal() }; 209 | 210 | memoryStream.Seek(0, SeekOrigin.Begin); 211 | 212 | Mock.Get(fileServiceMock).Setup(f => f.GetAttributes(testPath)).Returns(FileAttributes.Directory); 213 | Mock.Get(directoryServiceMock).Setup(d => d.GetFiles(testPath)).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 214 | Mock.Get(directoryServiceMock).Setup(d => d.EnumerateFiles(It.IsAny(), It.IsAny(), It.IsAny())).Returns(new string[] { "taco.docx", "cat.pptx", "foo.xlsx", "bar.docm" }); 215 | Mock.Get(functionUtilsMock).Setup(f => f.GetDocument(It.IsAny(), It.IsAny())).Returns(testWordDoc); 216 | Mock.Get(functionUtilsMock).Setup(f => f.GetValidationErrors(It.IsAny())).Returns(new Tuple>(false, validationErrorInfos)); 217 | 218 | // Act 219 | object validationErrors = validate.OOXML(testPath, testFormat, false, false); 220 | 221 | // Assert 222 | Assert.IsNotNull(validationErrors); 223 | Assert.AreEqual(validationErrors, "[{\"FilePath\":\"taco.docx\",\"ValidationErrors\":null},{\"FilePath\":\"cat.pptx\",\"ValidationErrors\":null},{\"FilePath\":\"foo.xlsx\",\"ValidationErrors\":null},{\"FilePath\":\"bar.docm\",\"ValidationErrors\":null}]"); 224 | Mock.Get(directoryServiceMock).Verify(d => d.EnumerateFiles(testPath, "*.*", SearchOption.AllDirectories), Times.Never); 225 | Mock.Get(directoryServiceMock).Verify(d => d.GetFiles(testPath), Times.Once); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /OOXMLValidatorCLITests/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Michael Bowen", 12 | "xmlHeader": false, 13 | "copyrightText": "Licensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", 14 | "variables": { 15 | "licenseName": "MIT", 16 | "licenseFile": "LICENSE" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test and Release](https://github.com/mikeebowen/OOXML-Validator/actions/workflows/dotnet.yml/badge.svg)](https://github.com/mikeebowen/OOXML-Validator/actions/workflows/dotnet.yml) 2 | 3 | # OOXML-Validator 4 | 5 | ## What is it? 6 | 7 | The OOXML Validator is a .NET CLI package which validates Open Office XML files (.docx, .docm, .dotm, .dotx, .pptx, .pptm, .potm, .potx, .ppam, .ppsm, .ppsx, .xlsx, .xlsm, .xltm, .xltx, or .xlam) and returns the validation errors as JSON or XML. 8 | 9 | ## How is it used? 10 | 11 | The OOXML Validator CLI accepts 1 required and 4 optional parameters. The first argument must be the file or directory path, the order of the other 4 arguments doesn't matter. 12 | 13 | Argument Order | Type | Value | Required/Optional 14 | ---|---|---|--- 15 | First (Required) | string | The absolute path to the file or folder to validate | Required 16 | Any | string | The version of Office to validate against* | Optional 17 | Any | string | If the value is `--xml` or `-x`, cli will return xml | Optional 18 | Any | string | If the value is `--recursive` or `-r` validates files recursively through all folders\** | Optional 19 | Any | string | If the value is `--all` or `-a` files without any errors are included in the list. | Optional 20 | 21 | \* Must be one of these (case sensitive): `Office2007`, `Office2010`, `Office2013`, `Office2016`, `Office2019`, `Office2021`, `Microsoft365`. Defaults to `Microsoft365` 22 | 23 | \** Only valid if the path passed is a directory and not a file, otherwise it is ignored 24 | 25 | ## XML vs JSON 26 | 27 | XML and JSON both return a list of validation errors. In addition to the validation error list, the XML data has 2 attributes: `FilePath` and `IsStrict`. FilePath is the path to the file that was validated and IsStrict is true if the document is saved in strict mode and false otherwise. 28 | 29 | ## Validating Directories 30 | 31 | If the first argument passed is a directory path, then all Office Open XML files in the directory are validated. Non-OOXML files are ignored. If the `-r` or `--recursive` flags are passed then all directories are validated recursively. 32 | 33 | 34 | ## Development 35 | You can run some development scripts with 36 | 37 | ```bash 38 | ./dev.sh help 39 | # ./dev.sh [build_env] 40 | # 41 | # help show this help message 42 | # docker docker container for development 43 | # build build 44 | # envs show build envs 45 | # run run the command line 46 | # 47 | ``` 48 | 49 | To build run the following, replacing `linux-x64` with your environment 50 | 51 | ```bash 52 | ./dev.sh build linux-x64 53 | ``` 54 | 55 | Run the executable with 56 | 57 | ```bash 58 | ./dev.sh run linux-x64 ./test.docx 59 | ``` 60 | 61 | If you don't want to install `dotnet` just run the following to start a container shell 62 | 63 | ```bash 64 | ./dev.sh docker 65 | # root@8b0f1aad055c:/code# [RUN YOUR COMMANDS HERE] 66 | ``` 67 | 68 | ### OSX with Apple Silicon (M1, M2, M3, etc chip) 69 | 70 | 1. use the docker method from above to start the project 71 | 2. from the docker CLI run the build for linux-arm64: 72 | 73 | `root@8b0f1aad055c:/code# ./dev.sh build linux-arm64` 74 | 75 | Use the CLI by calling: 76 | 77 | `root@8b0f1aad055c:/code# ./dev.sh run linux-arm64 [other-options]` 78 | 79 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | build_envs=( 5 | linux-x64 6 | linux-arm64 7 | osx-x64 8 | osx-arm64 9 | win-x64 10 | ) 11 | 12 | help () { 13 | cat < [build_env] 15 | 16 | help show this help message 17 | docker docker container for development 18 | build build 19 | envs show build envs 20 | run run the command line 21 | 22 | EOF 23 | } 24 | 25 | if [ "$#" -lt 1 ]; then 26 | help 27 | fi 28 | 29 | invalid_env () { 30 | echo "Error: '$1' must be one of '${build_envs[@]}'" 31 | help 32 | } 33 | 34 | command=$1; 35 | 36 | build () { 37 | if [ "$#" -ne 1 ]; then 38 | echo "Illegal number of parameters" 39 | help 40 | exit 1 41 | fi 42 | build_env=$1 43 | 44 | if ! [[ ${build_envs[@]} =~ $build_env ]]; then 45 | invalid_env $build_env 46 | exit 1 47 | fi 48 | 49 | case "$build_env" in 50 | "linux-x64") 51 | dotnet publish --framework net8.0 -c Release -r linux-x64 -p:IncludeNativeLibrariesForSelfExtract=true -p:InvariantGlobalization=true OOXMLValidator.sln 52 | ;; 53 | "linux-arm64") 54 | dotnet publish --framework net8.0 -c Release -r linux-arm64 -p:IncludeNativeLibrariesForSelfExtract=true -p:InvariantGlobalization=true OOXMLValidator.sln 55 | ;; 56 | "osx-x64") 57 | dotnet publish --framework net8.0 -c Release -r osx-x64 OOXMLValidator.sln 58 | ;; 59 | "osx-arm64") 60 | dotnet publish --framework net8.0 -c Release -r osx-arm64 OOXMLValidator.sln 61 | ;; 62 | "win-x64") 63 | dotnet publish --framework net8.0 -c Release -r win-x64 OOXMLValidator.sln 64 | ;; 65 | esac 66 | 67 | if ! [[ "$build_env" =~ "win-" ]]; then 68 | chmod +x "./OOXMLValidatorCLI/bin/Release/net8.0/${build_env}/publish/OOXMLValidatorCLI" 69 | fi 70 | } 71 | 72 | run () { 73 | if [ "$#" -lt 1 ]; then 74 | echo "Illegal number of parameters" 75 | help 76 | exit 1 77 | fi 78 | build_env=$1 79 | 80 | if ! [[ ${build_envs[@]} =~ $build_env ]]; then 81 | invalid_env "$build_env" 82 | exit 1 83 | fi 84 | 85 | if [[ "$build_env" =~ "win-" ]]; then 86 | ext=".exe" 87 | fi 88 | 89 | "./OOXMLValidatorCLI/bin/Release/net8.0/${build_env}/publish/OOXMLValidatorCLI${ext}" ${*:2} 90 | exit 0 91 | } 92 | 93 | test () { 94 | if [ "$#" -lt 1 ]; then 95 | echo "Illegal number of parameters" 96 | help 97 | exit 1 98 | fi 99 | build_env=$1 100 | 101 | if ! [[ ${build_envs[@]} =~ $build_env ]]; then 102 | invalid_env "$build_env" 103 | exit 1 104 | fi 105 | 106 | if [[ "$build_env" =~ "win-" ]]; then 107 | ext=".exe" 108 | fi 109 | 110 | shell_cmd="./${CI_SHELL_OVERRIDE:-"OOXMLValidatorCLI/bin/Release/net8.0/${build_env}/publish"}/OOXMLValidatorCLI${ext}" 111 | echo $shell_cmd 112 | chmod +x "${shell_cmd}" 113 | 114 | output="$($shell_cmd)" 115 | if [[ "$output" == "Value cannot be null." ]]; then 116 | echo "success" 117 | exit 0 118 | else 119 | echo "error:" 120 | echo "$output" 121 | exit 2 122 | fi 123 | 124 | } 125 | 126 | envs () { 127 | echo "${build_envs[@]}" 128 | } 129 | 130 | docker () { 131 | if [[ "$DOCKER_RUNNING" == "true" ]]; then 132 | echo "Error: Already running inside container" 133 | help 134 | exit 1 135 | fi 136 | 137 | cleanup() { 138 | rm .build-files/Dockerfile || true 139 | rm .build-files/compose.yaml || true 140 | rmdir .build-files || true 141 | } 142 | trap cleanup EXIT 143 | 144 | mkdir .build-files 145 | cat << EOF > .build-files/Dockerfile 146 | FROM ubuntu 147 | ENV DEBIAN_FRONTEND noninteractive 148 | 149 | RUN apt-get update && apt-get install -y dotnet-sdk-8.0 150 | 151 | WORKDIR /code 152 | EOF 153 | 154 | cat << EOF > .build-files/compose.yaml 155 | services: 156 | dev: 157 | build: 158 | dockerfile: ./Dockerfile 159 | environment: 160 | - DOCKER_RUNNING=true 161 | volumes: 162 | - ../:/code 163 | EOF 164 | 165 | docker-compose -f .build-files/compose.yaml run dev bash 166 | } 167 | 168 | case "$command" in 169 | "docker") 170 | docker 171 | ;; 172 | "build") 173 | build $2 174 | ;; 175 | "run") 176 | run ${*:2} 177 | ;; 178 | "envs") 179 | envs 180 | ;; 181 | "help") 182 | help 183 | ;; 184 | "test") 185 | test $2 186 | ;; 187 | esac 188 | --------------------------------------------------------------------------------