diff --git a/build.ps1 b/build.ps1 index 8a81fbddd..7b284837c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -262,6 +262,7 @@ if (!$SkipBuild) { "osinfo", "powershell-adapter", "process", + "resources/filesys", "runcommandonset", "tools/dsctest", "tools/test_group_resource", diff --git a/dsc/examples/faultydriver_detect.dsc.yaml b/dsc/examples/faultydriver_detect.dsc.yaml new file mode 100644 index 000000000..7d6715995 --- /dev/null +++ b/dsc/examples/faultydriver_detect.dsc.yaml @@ -0,0 +1,13 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: check for CrowdStrike driver + type: Microsoft.Windows/Driver + properties: + name: CSAgent + +- name: check if faulty driver of CrowdStrike exists under system32\drivers + type: Microsoft.DSC/FileSystem + properties: + path: "[path(systemRoot(), 'windows', 'system32', 'drivers', 'crowdstrike', 'C-00000291-00000000-00000026.sys')]" + hash: '6454cc4779c1c8387c547099181cfcbecc17401fb22750431c15a3b02f8243ea' diff --git a/dsc/examples/faultydriver_recovery.dsc.yaml b/dsc/examples/faultydriver_recovery.dsc.yaml new file mode 100644 index 000000000..ab9bba7ef --- /dev/null +++ b/dsc/examples/faultydriver_recovery.dsc.yaml @@ -0,0 +1,9 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json +resources: +- name: check if faulty driver of CrowdStrike exists under system32\drivers + type: Microsoft.DSC/FileSystem + properties: + path: "[path(systemRoot(), 'windows', 'system32', 'drivers', 'crowdstrike', 'C-00000291-00000000-00000026.sys')]" + _exist: False + hash: '6454cc4779c1c8387c547099181cfcbecc17401fb22750431c15a3b02f8243ea' diff --git a/dsc/examples/filesys_create.dsc.yaml b/dsc/examples/filesys_create.dsc.yaml new file mode 100644 index 000000000..3b93ea952 --- /dev/null +++ b/dsc/examples/filesys_create.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Create empty file + type: Microsoft.DSC/File + properties: + path: "[path(envvar('TEMP'), 'test-file-resource.txt')]" + _exist: true diff --git a/dsc/examples/filesys_create_parent.dsc.yaml b/dsc/examples/filesys_create_parent.dsc.yaml new file mode 100644 index 000000000..244af7e89 --- /dev/null +++ b/dsc/examples/filesys_create_parent.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Create empty file + type: Microsoft.DSC/File + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource', 'test-file-resource.txt')]" + _exist: true diff --git a/dsc/examples/filesys_delete.dsc.yaml b/dsc/examples/filesys_delete.dsc.yaml new file mode 100644 index 000000000..aefb61a64 --- /dev/null +++ b/dsc/examples/filesys_delete.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Create empty file + type: Microsoft.DSC/File + properties: + path: "[path(envvar('TEMP'), 'test-file-resource.txt')]" + _exist: false diff --git a/dsc/examples/filesys_dir_create.dsc.yaml b/dsc/examples/filesys_dir_create.dsc.yaml new file mode 100644 index 000000000..c6d91d1a8 --- /dev/null +++ b/dsc/examples/filesys_dir_create.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Create empty directory + type: Microsoft.DSC/Directory + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource')]" + _exist: true diff --git a/dsc/examples/filesys_dir_create_parent.dsc.yaml b/dsc/examples/filesys_dir_create_parent.dsc.yaml new file mode 100644 index 000000000..4502926f7 --- /dev/null +++ b/dsc/examples/filesys_dir_create_parent.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Create empty directory + type: Microsoft.DSC/Directory + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource', 'test-sub-dir-resource')]" + _exist: true diff --git a/dsc/examples/filesys_dir_delete.dsc.yaml b/dsc/examples/filesys_dir_delete.dsc.yaml new file mode 100644 index 000000000..d45b092fd --- /dev/null +++ b/dsc/examples/filesys_dir_delete.dsc.yaml @@ -0,0 +1,8 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Delete empty directory + type: Microsoft.DSC/Directory + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource')]" + _exist: false diff --git a/dsc/examples/filesys_dir_delete_recurse.dsc.yaml b/dsc/examples/filesys_dir_delete_recurse.dsc.yaml new file mode 100644 index 000000000..40bdeda81 --- /dev/null +++ b/dsc/examples/filesys_dir_delete_recurse.dsc.yaml @@ -0,0 +1,9 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Delete non-empty directory + type: Microsoft.DSC/Directory + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource')]" + recurse: true + _exist: false diff --git a/dsc/examples/filesys_filecontent.dsc.yaml b/dsc/examples/filesys_filecontent.dsc.yaml new file mode 100644 index 000000000..44faae1ec --- /dev/null +++ b/dsc/examples/filesys_filecontent.dsc.yaml @@ -0,0 +1,9 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Set file content + type: Microsoft.DSC/FileContent + properties: + path: "[path(envvar('TEMP'), 'test-file-resource.txt')]" + content: "Hello, World!" + _exist: true diff --git a/dsc/examples/filesys_filecontent_parent.dsc.yaml b/dsc/examples/filesys_filecontent_parent.dsc.yaml new file mode 100644 index 000000000..72174b8dc --- /dev/null +++ b/dsc/examples/filesys_filecontent_parent.dsc.yaml @@ -0,0 +1,9 @@ +# Example configuration mixing native app resources with classic PS resources +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Set file content + type: Microsoft.DSC/FileContent + properties: + path: "[path(envvar('TEMP'), 'test-dir-resource', 'test-file-resource.txt')]" + content: "Hello, World!" + _exist: true diff --git a/dsc/tests/filesys.tests.ps1 b/dsc/tests/filesys.tests.ps1 new file mode 100644 index 000000000..99c6bd75c --- /dev/null +++ b/dsc/tests/filesys.tests.ps1 @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'FileSys resoure tests' { + BeforeAll { + $testDir = Join-Path $env:TEMP 'test-dir-resource' + $testFile = Join-Path $testDir 'test-file-resource.txt' + $testFileName = 'test-file-resource.txt' + } + + It 'Filesys resource can create file' { + if (Test-Path $testFile) { + Remove-Item -Path $testFile -Force + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_create.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $path = $resultJson.results.result.afterState.path + $path | Should -Exist + Get-Item $resultJson.results.result.afterState.path | Should -BeOfType 'System.IO.FileInfo' + } + + It 'Filesys resource can create directory' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_dir_create.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultJson.results.result.afterState.path | Should -Exist + Get-Item $resultJson.results.result.afterState.path | Should -BeOfType 'System.IO.DirectoryInfo' + } + + It 'Filesys resource can create file with content' { + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_filecontent.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + + $resultFilePath = $resultJson.results.result.afterState.path + $resultFilePath | Should -Exist + Get-Content $resultFilePath | Should -Be "Hello, World!" + } + + It 'Filesys resource can delete a file' { + if (-not (Test-Path $testFile)) { + New-Item -Path $testFile -ItemType File -Force | Out-Null + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_delete.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultFilePath = $resultJson.results.result.afterState.path + $resultFilePath | Should -Not -Exist + } + + It 'Filesys resource can delete an empty directory' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + New-Item -Path $testDir -ItemType Directory -Force | Out-Null + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_dir_delete.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultDirPath = $resultJson.results.result.afterState.path + $resultDirPath | Should -Not -Exist + } + + It 'Filesys resource cannot delete a non-empty directory' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + New-Item -Path $testDir -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $testDir $testFileName) -ItemType File -Force | Out-Null + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_dir_delete.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Not -Be 0 + $testDir | Should -Exist + } + + It 'Filesys resource can delete a directory recursively' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + $dirPath = New-Item -Path $testDir -ItemType Directory -Force + $subDirPath = New-Item -Path (Join-Path $dirPath 'test-subdir') -ItemType Directory -Force + New-Item -Path (Join-Path $subDirPath $testFileName) -ItemType File -Force | Out-Null + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_dir_delete_recurse.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultDirPath = $resultJson.results.result.afterState.path + $resultDirPath | Should -Not -Exist + } + + It 'Can create file if parent directory does not exist' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_create_parent.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultJson.results.result.afterState.path | Should -Exist + Get-Item $resultJson.results.result.afterState.path | Should -BeOfType 'System.IO.FileInfo' + } + + It 'Can create file with content if parent directory does not exist' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_filecontent_parent.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + + $resultFilePath = $resultJson.results.result.afterState.path + $resultFilePath | Should -Exist + Get-Content $resultFilePath | Should -Be "Hello, World!" + } + + It 'Can create directory if parent directory does not exist' { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Force -Recurse + } + + $resultJson = dsc config set -f "$PSScriptRoot/../examples/filesys_dir_create_parent.dsc.yaml" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $resultJson.hadErrors | Should -BeFalse + $resultJson.results.result.afterState.path | Should -Exist + Get-Item $resultJson.results.result.afterState.path | Should -BeOfType 'System.IO.DirectoryInfo' + } +} \ No newline at end of file diff --git a/pal/Cargo.toml b/pal/Cargo.toml index a75d6c5bd..ab00c3b5b 100644 --- a/pal/Cargo.toml +++ b/pal/Cargo.toml @@ -13,4 +13,4 @@ opt-level = 2 lto = true [build-dependencies] -cc = "1.1" +cc = "~1.1" diff --git a/resources/filesys/Cargo.lock b/resources/filesys/Cargo.lock new file mode 100644 index 000000000..242a87142 --- /dev/null +++ b/resources/filesys/Cargo.lock @@ -0,0 +1,593 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "filesys" +version = "0.1.0" +dependencies = [ + "clap", + "encoding_rs", + "encoding_rs_io", + "fs_extra", + "schemars", + "serde", + "serde_json", + "sha256", + "tracing", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "pin-project-lite", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/resources/filesys/Cargo.toml b/resources/filesys/Cargo.toml new file mode 100644 index 000000000..361035053 --- /dev/null +++ b/resources/filesys/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "filesys" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version ="4.5", features = ["derive"] } +encoding_rs = "0.8.35" +encoding_rs_io = "0.1.7" +serde = "1.0" +serde_json = "1.0" +schemars = "0.8" +tracing = { version = "0.1" } +sha256 = { version = "1.5.0" } +fs_extra = { version = "1.2.0" } diff --git a/resources/filesys/directory.dsc.resource.json b/resources/filesys/directory.dsc.resource.json new file mode 100644 index 000000000..1d3502c56 --- /dev/null +++ b/resources/filesys/directory.dsc.resource.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.DSC/Directory", + "description": "Manage directory configuration settings.", + "version": "0.1.0", + "get": { + "executable": "filesys", + "args": [ + "get", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "directory" + ], + "input": "stdin", + "implementsPretest": false + }, + "set": { + "executable": "filesys", + "args": [ + "set", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "directory" + ], + "input": "stdin" + }, + "delete": { + "executable": "filesys", + "args": [ + "delete", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "directory" + ], + "input": "stdin" + }, + "export": { + "executable": "filesys", + "args": [ + "export", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "directory" + ], + "input": "stdin" + }, + "schema": { + "command": { + "executable": "filesys", + "args": [ + "schema", + "--schema-type", + "directory" + ] + } + } +} diff --git a/resources/filesys/file.dsc.resource.json b/resources/filesys/file.dsc.resource.json new file mode 100644 index 000000000..1bdcab38e --- /dev/null +++ b/resources/filesys/file.dsc.resource.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.DSC/File", + "description": "Manage file system configuration settings.", + "version": "0.1.0", + "get": { + "executable": "filesys", + "args": [ + "get", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file" + ], + "input": "stdin", + "implementsPretest": false + }, + "set": { + "executable": "filesys", + "args": [ + "set", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file" + ], + "input": "stdin" + }, + "delete": { + "executable": "filesys", + "args": [ + "delete", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file" + ], + "input": "stdin" + }, + "export": { + "executable": "filesys", + "args": [ + "export", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file" + ], + "input": "stdin" + }, + "schema": { + "command": { + "executable": "filesys", + "args": [ + "schema", + "--schema-type", + "file" + ] + } + } +} diff --git a/resources/filesys/filecontent.dsc.resource.json b/resources/filesys/filecontent.dsc.resource.json new file mode 100644 index 000000000..8e86ba8e1 --- /dev/null +++ b/resources/filesys/filecontent.dsc.resource.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.DSC/FileContent", + "description": "Manage file content configuration settings.", + "version": "0.1.0", + "get": { + "executable": "filesys", + "args": [ + "get", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file-content" + ], + "input": "stdin", + "implementsPretest": false + }, + "set": { + "executable": "filesys", + "args": [ + "set", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file-content" + ], + "input": "stdin" + }, + "delete": { + "executable": "filesys", + "args": [ + "delete", + { + "jsonInputArg": "--input", + "mandatory": true + }, + "--schema-type", + "file-content" + ], + "input": "stdin" + }, + "schema": { + "command": { + "executable": "filesys", + "args": [ + "schema", + "--schema-type", + "file-content" + ] + } + } +} diff --git a/resources/filesys/src/args.rs b/resources/filesys/src/args.rs new file mode 100644 index 000000000..3e9bb5b62 --- /dev/null +++ b/resources/filesys/src/args.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[clap(name = "file", version = "1.0", about = "Manage state of a file on disk.", long_about = None)] + +pub struct Args { + #[clap(subcommand)] + pub subcommand: SubCommand +} + +#[derive(Debug, PartialEq, Eq, Subcommand)] +pub enum SubCommand { + #[clap(name = "get", about = "Get the current state of the file.", arg_required_else_help = true)] + Get { + #[clap(short, long, required = true, help = "The path to the file.")] + input: String, + #[clap(short, long, default_value = "file", help = "The type of file system resource.")] + schema_type: FileSystemObjectType, + }, + + #[clap(name = "delete", about = "Delete the file on disk.", arg_required_else_help = true)] + Delete { + #[clap(short, long, required = true, help = "The path to the file.")] + input: String, + #[clap(short, long, default_value = "file", help = "The type of file system resource.")] + schema_type: FileSystemObjectType, + }, + + #[clap(name = "set", about = "Set the current state of file or directory.", arg_required_else_help = true)] + Set { + #[clap(short, long, required = true, help = "The path to the file or directory.")] + input : String, + #[clap(short, long, default_value = "file", help = "The type of file system resource.")] + schema_type: FileSystemObjectType, + }, + + #[clap(name = "export", about = "Exports the files and directories under the specified path", arg_required_else_help = true)] + Export { + #[clap(short, long, required = true, help = "The path to the file or directory.")] + input: String, + #[clap(short, long, default_value = "file", help = "The type of file system resource.")] + schema_type: FileSystemObjectType, + }, + + #[clap(name = "schema", about = "Retrieve JSON schema.")] + Schema { + #[clap(short, long, default_value = "file", help = "The type of schema to retrieve.")] + schema_type: FileSystemObjectType, + } +} + +#[derive(Clone, Default, Debug, PartialEq, Eq, clap::ValueEnum)] +pub enum FileSystemObjectType { + #[default] + File, + Directory, + FileContent, +} diff --git a/resources/filesys/src/config.rs b/resources/filesys/src/config.rs new file mode 100644 index 000000000..b718d0824 --- /dev/null +++ b/resources/filesys/src/config.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Hash)] +#[serde(rename ="File", deny_unknown_fields)] +pub struct File { + /// The path to the file. + pub path: String, + + /// The file size. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// The file hash. + pub hash: Option, + + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] + pub exist: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Hash)] +#[serde(rename ="Directory", deny_unknown_fields)] +pub struct Directory { + /// The path to the directory. + pub path: String, + + /// The directory size. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// The files under the directory. + #[serde(skip_serializing_if = "Option::is_none")] + pub files: Option>, + + /// Recurse into subdirectories. + #[serde(skip_serializing_if = "Option::is_none")] + pub recurse: Option, + + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] + pub exist: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Hash)] +#[serde(rename ="FileContent", deny_unknown_fields)] +pub struct FileContent +{ + /// The path to the file. + pub path: String, + + /// The file hash. If not provided, the hash is calculated from the content. + #[serde(skip_serializing_if = "Option::is_none")] + pub hash: Option, + + /// The file encoding. UTF-8 is the default. + #[serde(skip_serializing_if = "Option::is_none")] + pub encoding: Option, + + /// The file content. + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + + /// If the file exists. True is the default. + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] + pub exist: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Hash)] +pub enum Encoding { + Utf8, + Utf16, + Ascii, + Binary, +} diff --git a/resources/filesys/src/dir_helpers.rs b/resources/filesys/src/dir_helpers.rs new file mode 100644 index 000000000..ae16a3053 --- /dev/null +++ b/resources/filesys/src/dir_helpers.rs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::config::File; +use crate::config::Directory; +use crate::file_helper::get_file; +use std::fs; +use std::path::Path; +use tracing::{debug}; +use fs_extra::dir::get_size; + +impl Directory { + /// Create a new `Directory`. + /// + /// # Arguments + /// + /// * `string` - The string for the Path + #[must_use] + pub fn new(path: &str) -> Directory { + Directory { + path: path.to_string(), + size: None, + files: None, + recurse: Some(false), + exist: None, + } + } +} + +pub fn get_dir(dir: &Directory) -> Result> { + debug!("In get_dir"); + match compare_dir_state(dir) { + Ok(d) => { + Ok(d) + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn set_dir(dir: &Directory) -> Result> { + match compare_dir_state(dir) { + Ok(current_dir) => { + debug!("In set_dir"); + debug!("dir exist {:?}", dir.exist); + debug!("expected dir exist {:?}", dir.exist.unwrap_or(true)); + + match (current_dir.exist.unwrap_or(true), dir.exist.unwrap_or(true)) { + // if the current dir exists and expected state is exist == true, do nothing + (true, true) | (false, false) => { + return Ok(current_dir); + } + + // if the current dir exists and expected state is exist == true, create it + (true, false) => { + debug!("Deleting directory: {:?}", dir.path); + + if dir.recurse.unwrap_or(false) { + fs::remove_dir_all(dir.path.as_str())?; + } else { + fs::remove_dir(dir.path.as_str())?; + } + + return Ok(get_dir(&dir)?) + } + + // if the current dir does not exist and expected state is exist == true, create it + (false, true) => { + debug!("Creating directory: {:?}", dir.path); + fs::create_dir_all(dir.path.as_str())?; + return Ok(get_dir(&dir)?) + } + } + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn export_dir_path(dir: &Directory) -> Result> { + // Export the file or directory + let path = Path::new(dir.path.as_str()); + + match path.exists() { + false => { + return Ok(Directory { path: path.to_str().unwrap().to_string(), size: None, files: None, recurse: dir.recurse, exist: Some(false) }); + } + _ => {} + } + + match path.is_dir() { + true => { + let files: Vec = { + let dir = fs::read_dir(path)?; + let mut files = Vec::new(); + for entry in dir { + let entry = entry?; + let path = entry.path(); + let f = File::new(path.to_str().unwrap()); + files.push(get_file(&f)?); + } + files + }; + + let dir_size = get_size(path)?; + + Ok(Directory { path: path.to_str().unwrap().to_string(), size: Some(dir_size), files: Some(files), recurse: dir.recurse, exist: Some(true) }) + } + false => { + let path = Path::new(path); + let f = File::new(path.to_str().unwrap()); + let file = get_file(&f)?; + let parent = path.parent(); + match parent { + Some(parent) => { + Ok(Directory { path: parent.to_str().unwrap().to_string(), size: file.size, files: vec![file].into(), recurse: dir.recurse, exist: Some(true) }) + } + _ => { + return Err("Path is not a file or directory")?; + } + } + } + } +} + +pub fn delete_dir(dir: &Directory) -> Result<(), Box> { + match compare_dir_state(dir) { + Ok(d) => { + + if d.exist == Some(false) { + return Ok(()); + } + + if d.recurse == Some(true) { + debug!("Deleting directory: {:?}", d.path); + fs::remove_dir_all(d.path)?; + return Ok(()); + } + else { + debug!("Deleting directory: {:?}", d.path); + + // Check if the directory is empty + let entries = fs::read_dir(d.path.to_string())?; + let mut is_empty = true; + for entry in entries { + let entry = entry?; + if entry.path().is_dir() { + is_empty = false; + break; + } + } + + if !is_empty { + return Err("Directory is not empty")?; + } + + fs::remove_dir(d.path)?; + return Ok(()); + } + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn compare_dir_state(dir: &Directory) -> Result> { + let path = Path::new(dir.path.as_str()); + + match path.exists() { + false => { + return Ok(Directory { path: path.to_str().unwrap().to_string(), size: None, files: None, recurse: dir.recurse, exist: Some(false) }); + } + true => { + match path.is_dir() { + false => { + return Err("Path is not a directory")?; + } + _ => {} + } + } + } + + let dir_size = get_size(path)?; + + match dir.size { + Some(size) => { + if size != dir_size { + Ok(Directory { path: path.to_str().unwrap().to_string(), size: Some(dir_size), files: None, recurse: dir.recurse, exist: Some(true) }) + } else { + Ok(Directory { path: path.to_str().unwrap().to_string(), size: Some(dir_size), files: None, recurse: dir.recurse, exist: Some(true) }) + } + } + None => { + Ok(Directory { path: path.to_str().unwrap().to_string(), size: Some(dir_size), files: None, recurse: dir.recurse, exist: Some(true) }) + } + } +} \ No newline at end of file diff --git a/resources/filesys/src/file_helper.rs b/resources/filesys/src/file_helper.rs new file mode 100644 index 000000000..7b29c436f --- /dev/null +++ b/resources/filesys/src/file_helper.rs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::config::File; +use std::fs; +use std::fs::File as fsFile; +use std::path::Path; +use tracing::{debug}; + +impl File { + /// Create a new `File`. + /// + /// # Arguments + /// + /// * `string` - The string for the Path + #[must_use] + pub fn new(path: &str) -> File { + File { + path: path.to_string(), + size: None, + hash: None, + exist: None, + } + } +} + +pub fn get_file(file: &File) -> Result> { + debug!("In get_file"); + match compare_file_state(file) { + Ok(f) => { + Ok(f) + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn set_file(file: &File) -> Result> { + match compare_file_state(file) { + Ok(_) => { + debug!("In set_file"); + debug!("file exist {:?}", file_exists(file.path.as_str())); + debug!("expected file exist {:?}", file.exist.unwrap_or(true)); + + let resolved_path = Path::new(file.path.as_str()); + + match (file_exists(file.path.as_str()), file.exist.unwrap_or(true)) { + + // if the current file exists and expected state is exist == false, delete it + (true, false) => { + debug!("Deleting file: {:?}", resolved_path); + fs::remove_file(resolved_path)?; + Ok(get_file(&file)?) + } + + // if the current file does not exist and expected state is exist == true, create it + (false, true) => { + debug!("Creating file: {:?}", resolved_path); + + if let Some(parent) = resolved_path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + fsFile::create(resolved_path)?; + let new_file = File::new(file.path.as_str()); + + Ok(get_file(&new_file)?) + } + + // if the current file exists and expected state is exist == true or both are false update and return + (true, true) | (false, false) => { + debug!("Updating file: {:?}", resolved_path); + let new_file = File::new(file.path.as_str()); + Ok(get_file(&new_file)?) + } + } + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn export_file_path(file: &File) -> Result> { + match compare_file_state(file) { + Ok(f) => { + Ok(f) + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn delete_file(file: &File) -> Result<(), Box> { + match compare_file_state(file) { + Ok(f) => { + debug!("Deleting file: {:?}", f.path); + fs::remove_file(f.path)?; + Ok(()) + }, + Err(e) => { + Err(e)? + } + } +} + +fn compare_file_state(file: &File) -> Result> { + let resolved_path = Path::new(file.path.as_str()); + debug!("Resolved path: {:?}", resolved_path); + match resolved_path.is_dir() { + true => { + return Err("Path is a directory")? + } + false => {} + } + let f: fsFile = match fsFile::open(resolved_path) { + Ok(f) => { + debug!("File found: {:?}", resolved_path); + f + }, + Err(e) => { + debug!("Error: {:?}", e); + if e.kind() == std::io::ErrorKind::NotFound { + debug!("File not found: {:?}", file.path); + let mut updated_file = file.clone(); + updated_file.exist = Some(false); + return Ok(updated_file) + } else { + return Err(e)? + } + } + }; + + let hash = calculate_hash(resolved_path.to_str().unwrap())?; + + match file.hash.as_ref() { + Some(h) => { + if h.to_lowercase() != hash.to_lowercase() { + debug!("Hash mismatch"); + let mut updated_file = file.clone(); + updated_file.exist = Some(false); + return Ok(updated_file) + } + else { + let metadata = f.metadata()?; + let mut updated_file = file.clone(); + updated_file.size = Some(metadata.len()); + updated_file.exist = Some(true); + return Ok(updated_file) + } + } + None => { + let metadata = f.metadata()?; + let mut updated_file = file.clone(); + updated_file.hash = Some(hash); + updated_file.size = Some(metadata.len()); + updated_file.exist = Some(true); + return Ok(updated_file) + } + }; +} + +pub fn calculate_hash(path: &str) -> Result> { + let bytes = fs::read(path)?; + let digest = sha256::digest(&bytes); + Ok(digest) +} + +fn file_exists(path: &str) -> bool { + return Path::new(path).exists(); +} diff --git a/resources/filesys/src/filecontent_helper.rs b/resources/filesys/src/filecontent_helper.rs new file mode 100644 index 000000000..050aca5c8 --- /dev/null +++ b/resources/filesys/src/filecontent_helper.rs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::config::{FileContent, Encoding}; +use std::fs; +use std::path::Path; +use std::io; +use std::io::Write; +use tracing::{debug}; + +impl Encoding { + pub fn to_encoding_rs(&self) -> Option<&'static encoding_rs::Encoding> { + match self { + Encoding::Utf8 => Some(encoding_rs::UTF_8), + Encoding::Utf16 => Some(encoding_rs::UTF_16LE), // or UTF_16BE depending on your needs + Encoding::Ascii => Some(encoding_rs::WINDOWS_1252), // ASCII is a subset of Windows-1252 + Encoding::Binary => None, + } + } +} + +impl FileContent { + /// Create a new `FileContent`. + /// + /// # Arguments + /// + /// * `string` - The string for the Path + #[must_use] + pub fn new(path: &str) -> FileContent { + FileContent { + path: path.to_string(), + content: None, + hash: None, + encoding: Some(Encoding::Utf8), + exist: None, + } + } +} + +pub fn get_file_content(filecontent: &FileContent) -> Result> { + debug!("In get_file_content"); + match compare_filecontent_state(filecontent) { + Ok(f) => { + Ok(f) + }, + Err(e) => { + Err(e)? + } + } +} + +pub fn set_file_content(filecontent: &FileContent) -> Result> { + debug!("In set_file_content"); + let path = Path::new(&filecontent.path); + //let content = filecontent.content.as_ref().unwrap_or(&String::new()); + //let encoding = filecontent.encoding.unwrap_or(Encoding::Utf8).to_encoding_rs().unwrap_or(encoding_rs::UTF_8); + let file_expected_exists = filecontent.exist.unwrap_or(true); + + if path.exists() && !file_expected_exists { + std::fs::remove_file(path)?; + return Ok(filecontent.clone()) + } + else if !path.exists() && !file_expected_exists { + return Ok(filecontent.clone()) + } + + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + let mut file = std::fs::File::create(path)?; + + // let mut encoder = encoding.new_encoder(); + // let mut bytes = vec![0; content.len() * encoding.new_encoder().max_buffer_length()]; + // let (bytes_written, _, _) = encoder.encode_to_slice(content, &mut bytes, true); + // file.write_all(&bytes[..bytes_written])?; + + match &filecontent.content { + Some(content) => { + file.write_all(content.as_bytes())?; + }, + None => { + } + } + + Ok(filecontent.clone()) +} + +pub fn delete_file_content(filecontent: &FileContent) -> Result> { + debug!("In delete_file_content"); + let path = Path::new(&filecontent.path); + + let mut filecontent_to_delete = filecontent.clone(); + + if path.exists() { + + filecontent_to_delete.exist = Some(false); + filecontent_to_delete.content = None; + set_file_content(&filecontent)?; + } + + Ok(filecontent_to_delete) +} + +pub fn read_file_with_encoding(path: &Path, encoding: &'static encoding_rs::Encoding) -> io::Result { + let bytes = std::fs::read(path)?; + let (decoded_str, _encoding_used, had_errors) = encoding.decode(&bytes); + + if had_errors { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid encoding")); + } + + Ok(decoded_str.to_string()) +} + +pub fn compare_filecontent_state(filecontent: &FileContent) -> Result> { + debug!("In compare_filecontent_state"); + + let rs_encoding = filecontent.encoding.as_ref().unwrap_or(&Encoding::Utf8).to_encoding_rs().unwrap_or(encoding_rs::UTF_8); + + let path = Path::new(&filecontent.path); + if path.exists() { + let content = read_file_with_encoding(path, rs_encoding)?; + let content_hash = sha256::digest(content.as_bytes()); + let hash = filecontent.hash.as_ref().unwrap_or(&content_hash); + + match filecontent.hash.as_ref() { + Some(h) => { + if h.to_lowercase() == hash.to_lowercase() { + let mut updated_file_content = filecontent.clone(); + updated_file_content.hash = Some(hash.to_string()); + updated_file_content.content = Some(content); + updated_file_content.exist = Some(true); + + return Ok(updated_file_content) + } + else { + return Err("Hash does not match")?; + } + }, + None => { + let mut updated_file_content = filecontent.clone(); + updated_file_content.hash = Some(hash.to_string()); + updated_file_content.content = Some(content); + updated_file_content.exist = Some(true); + + return Ok(updated_file_content) + } + } + } + else { + match filecontent.exist { + Some(true) | None => { + let mut updated_file_content = filecontent.clone(); + updated_file_content.exist = Some(false); + return Ok(updated_file_content); + }, + Some(false) => { + return Ok(filecontent.clone()); + } + } + } +} diff --git a/resources/filesys/src/main.rs b/resources/filesys/src/main.rs new file mode 100644 index 000000000..45c329b42 --- /dev/null +++ b/resources/filesys/src/main.rs @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use args::Args; +use clap::Parser; +use std::process::exit; +use tracing::{debug, error}; +use crate::config::{File, Directory, FileContent}; +use file_helper::{get_file, set_file, delete_file, export_file_path}; +use dir_helpers::{get_dir, set_dir, delete_dir, export_dir_path}; +use filecontent_helper::{get_file_content, set_file_content, delete_file_content}; +use schemars::schema_for; + +mod args; +pub mod config; +mod file_helper; +mod dir_helpers; +mod filecontent_helper; + +const EXIT_SUCCESS: i32 = 0; +const EXIT_INVALID_INPUT: i32 = 1; +const EXIT_JSON_SERIALIZATION_FAILED: i32 = 2; + +fn main() { + let args = Args::parse(); + match args.subcommand { + args::SubCommand::Get { input, schema_type } => { + debug!("Getting at path: {}", input); + match schema_type { + args::FileSystemObjectType::File => { + debug!("Getting file at path: {}", input); + match parse_file(input) { + Some(parsed_file) => { + match get_file(&parsed_file) { + Ok(file) => { + match serde_json::to_string(&file) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + Err(e) => { + error!("Failed to get file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::Directory => { + debug!("Getting directory at path: {}", input); + match parse_directory(input) { + Some(parsed_directory) => { + match get_dir(&parsed_directory) { + Ok(dir) => { + match serde_json::to_string(&dir) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize directory: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + Err(e) => { + error!("Failed to get directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for directory."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::FileContent => { + debug!("Getting file content at path: {}", input); + match parse_filecontent(input) { + Some(parsed_filecontent) => { + match get_file_content(&parsed_filecontent) { + Ok(filecontent) => { + match serde_json::to_string(&filecontent) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file content: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + Err(e) => { + error!("Failed to get file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file content."); + exit(EXIT_INVALID_INPUT); + } + } + } + }; + } + + args::SubCommand::Delete { input, schema_type} => { + debug!("Deleting file at path: {}", input); + + match schema_type { + args::FileSystemObjectType::File => { + debug!("Deleting file at path: {}", input); + match parse_file(input) { + Some(parsed_file) => { + match delete_file(&parsed_file) { + Ok(file) => { + match serde_json::to_string(&file) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to delete file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::Directory => { + debug!("Deleting directory at path: {}", input); + match parse_directory(input) { + Some(parsed_directory) => { + match delete_dir(&parsed_directory) { + Ok(dir) => { + match serde_json::to_string(&dir) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to delete directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for directory."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::FileContent => { + debug!("Deleting file content at path: {}", input); + match parse_filecontent(input) { + Some(parsed_filecontent) => { + match delete_file_content(&parsed_filecontent) { + Ok(filecontent) => { + match serde_json::to_string(&filecontent) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to delete file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file content."); + exit(EXIT_INVALID_INPUT); + } + } + } + }; + } + args::SubCommand::Set { input, schema_type } => { + debug!("Setting file at path: {}", input); + match schema_type { + args::FileSystemObjectType::File => { + debug!("Setting file at path: {}", input); + match parse_file(input) { + Some(parsed_file) => { + match set_file(&parsed_file) { + Ok(file) => { + match serde_json::to_string(&file) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to set file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::Directory => { + debug!("Setting directory at path: {}", input); + match parse_directory(input) { + Some(parsed_directory) => { + match set_dir(&parsed_directory) { + Ok(dir) => { + match serde_json::to_string(&dir) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to set directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for directory."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::FileContent => { + debug!("Setting file content at path: {}", input); + match parse_filecontent(input) { + Some(parsed_filecontent) => { + match set_file_content(&parsed_filecontent) { + Ok(filecontent) => { + match serde_json::to_string(&filecontent) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to set file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file content."); + exit(EXIT_INVALID_INPUT); + } + } + } + }; + } + args::SubCommand::Export { input, schema_type } => { + debug!("Exporting file at path: {}", input); + + match schema_type { + args::FileSystemObjectType::File => { + debug!("Exporting file at path: {}", input); + match parse_file(input) { + Some(parsed_file) => { + match export_file_path(&parsed_file) { + Ok(file) => { + match serde_json::to_string(&file) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to export file: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::Directory => { + debug!("Exporting directory at path: {}", input); + match parse_directory(input) { + Some(parsed_directory) => { + match export_dir_path(&parsed_directory) { + Ok(dir) => { + match serde_json::to_string(&dir) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to export directory: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for directory."); + exit(EXIT_INVALID_INPUT); + } + } + } + args::FileSystemObjectType::FileContent => { + debug!("Exporting file content at path: {}", input); + match parse_filecontent(input) { + Some(parsed_filecontent) => { + match get_file_content(&parsed_filecontent) { + Ok(filecontent) => { + match serde_json::to_string(&filecontent) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + Err(e) => { + error!("Failed to export file content: {}", e); + exit(EXIT_INVALID_INPUT); + } + } + } + None => { + error!("Invalid input for file content."); + exit(EXIT_INVALID_INPUT); + } + } + } + }; + } + args::SubCommand::Schema { schema_type }=> { + match schema_type { + args::FileSystemObjectType::File => { + let schema = schema_for!(File); + match serde_json::to_string(&schema) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file schema: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + args::FileSystemObjectType::Directory => { + let schema = schema_for!(Directory); + match serde_json::to_string(&schema){ + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize directory schema: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + args::FileSystemObjectType::FileContent => { + let schema = schema_for!(FileContent); + match serde_json::to_string(&schema) { + Ok(json) => println!("{}", json), + Err(e) => { + error!("Failed to serialize file content schema: {}", e); + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + } + } + } + } + } + + exit(EXIT_SUCCESS); +} + +fn parse_file(input: String) -> Option { + let file: File = match serde_json::from_str(input.to_string().as_str()) { + Ok(input) => input, + Err(_) => return None, + }; + + Some(file) +} + +fn parse_directory(input: String) -> Option { + let dir: Directory = match serde_json::from_str(input.to_string().as_str()) { + Ok(input) => input, + Err(_) => return None, + }; + + Some(dir) +} + +fn parse_filecontent(input: String) -> Option { + let filecontent: FileContent = match serde_json::from_str(input.to_string().as_str()) { + Ok(input) => input, + Err(_) => return None, + }; + + Some(filecontent) +} \ No newline at end of file