diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b606e5e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*\bin
+*\obj
+*\gen
\ No newline at end of file
diff --git a/Microsoft.PowerShell.Archive.sln b/Microsoft.PowerShell.Archive.sln
new file mode 100644
index 0000000..a153782
--- /dev/null
+++ b/Microsoft.PowerShell.Archive.sln
@@ -0,0 +1,27 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.10
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerShell.Archive", "SRC\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.csproj", "{B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DCDF473-9667-4F84-B1A3-D3843766DAE8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BB182DC0-D6FC-4C77-807F-2F4618457F7C}
+ EndGlobalSection
+EndGlobal
diff --git a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.ArchiveItemInfo.Format.ps1xml b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.ArchiveItemInfo.Format.ps1xml
new file mode 100644
index 0000000..8ec16de
--- /dev/null
+++ b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.ArchiveItemInfo.Format.ps1xml
@@ -0,0 +1,44 @@
+
+
+
+
+ Microsoft.PowerShell.Archive.ArchiveItemInfo
+
+ Microsoft.PowerShell.Archive.ArchiveItemInfo
+
+
+
+
+ 25
+
+
+ 8
+
+
+ 25
+
+
+
+
+
+
+
+
+ LastWriteTime
+
+
+ Length
+
+
+ Name
+
+
+ FullName
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1 b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1
index 9da99cc..0754dd3 100644
--- a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1
+++ b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psd1
@@ -12,4 +12,5 @@ CmdletsToExport = @()
AliasesToExport = @()
NestedModules="Microsoft.PowerShell.Archive.psm1"
HelpInfoURI = 'https://go.microsoft.com/fwlink/?LinkId=393254'
+FormatsToProcess = @('Microsoft.PowerShell.Archive.ArchiveItemInfo.Format.ps1xml')
}
diff --git a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1
index 2ed4c84..8c408eb 100644
--- a/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1
+++ b/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1
@@ -1310,3 +1310,13 @@ function ArchivePathCompareHelper
return $normalizedPathArgA -eq $normalizedPathArgB
}
+
+
+
+if ($PSVersionTable.PSEdition -eq "Core") {
+ Write-Host -Object "Microsoft.PowerShell.Archive PSProvider is now Available in pwsh Core" -ForegroundColor Cyan
+ Import-Module "$PSScriptRoot\bin\Microsoft.PowerShell.Archive.dll"
+}
+else {
+ Write-Host -Object "Microsoft.PowerShell.Archive PSProvider is now Available in pwsh Core" -ForegroundColor Cyan
+}
diff --git a/Tests/PSProvider/Add-Content.Tests.ps1 b/Tests/PSProvider/Add-Content.Tests.ps1
new file mode 100644
index 0000000..1ad1ed4
--- /dev/null
+++ b/Tests/PSProvider/Add-Content.Tests.ps1
@@ -0,0 +1,81 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+Describe "Add-Content cmdlet tests" -Tags "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name TestDrive -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+ #Current tests run persistanence for zipfile.
+ Remove-Item TestDrive:\file* -ErrorAction Continue
+ Remove-Item TestDrive:\dynamic* -ErrorAction Continue
+
+ $file1 = "file1.txt"
+ New-Item -Path "TestDrive:\$file1" -ItemType File -Force
+ }
+
+ Context "Add-Content should actually add content" {
+ It "should Add-Content to TestDrive:\$file1" {
+ $result = Add-Content -Path TestDrive:\$file1 -Value "ExpectedContent" -PassThru
+ $result | Should -BeExactly "ExpectedContent"
+ }
+
+ It "should return expected string from TestDrive:\$file1" {
+ $result = Get-Content -Path TestDrive:\$file1
+ $result | Should -BeExactly "ExpectedContent"
+ }
+
+ It "should Add-Content to TestDrive:\dynamicfile.txt with dynamic parameters" -Pending:($IsLinux -Or $IsMacOS) {#https://github.com/PowerShell/PowerShell/issues/891
+ $result = Add-Content -Path TestDrive:\dynamicfile.txt -Value "ExpectedContent" -PassThru
+ $result | Should -BeExactly "ExpectedContent"
+ }
+
+ It "should return expected string from TestDrive:\dynamicfile.txt" -Pending:($IsLinux -Or $IsMacOS) {#https://github.com/PowerShell/PowerShell/issues/891
+ $result = Get-Content -Path TestDrive:\dynamicfile.txt
+ $result | Should -BeExactly "ExpectedContent"
+ }
+
+ It "should Add-Content to TestDrive:\$file1 even when -Value is `$null" {
+ $AsItWas = Get-Content -Path TestDrive:\$file1
+ { Add-Content -Path TestDrive:\$file1 -Value $null -ErrorAction Stop } | Should -Not -Throw
+ Get-Content -Path TestDrive:\$file1 | Should -BeExactly $AsItWas
+ }
+
+ It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$null" {
+ { Add-Content -Path $null -Value "ShouldNotWorkBecausePathIsNull" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.AddContentCommand"
+ }
+
+ #[BugId(BugDatabase.WindowsOutOfBandReleases, 903880)]
+ It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$()" {
+ { Add-Content -Path $() -Value "ShouldNotWorkBecausePathIsInvalid" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.AddContentCommand"
+ }
+
+ It "Should throw an error on a directory" {
+ { Add-Content -Path . -Value "WriteContainerContentException" -ErrorAction Stop } | Should -Throw -ErrorId "WriteContainerContentException,Microsoft.PowerShell.Commands.AddContentCommand"
+ }
+
+ #[BugId(BugDatabase.WindowsOutOfBandReleases, 9058182)]
+ It "should be able to pass multiple [string]`$objects to Add-Content through the pipeline to output a dynamic Path file" -Pending:($IsLinux -Or $IsMacOS) {#https://github.com/PowerShell/PowerShell/issues/891
+ "hello","world" | Add-Content -Path TestDrive:\dynamicfile2.txt
+ $result = Get-Content -Path TestDrive:\dynamicfile2.txt
+ $result.length | Should -Be 2
+ $result[0] | Should -BeExactly "hello"
+ $result[1] | Should -BeExactly "world"
+ }
+
+ It "Should not block reads while writing" {
+ $logpath = Join-Path $testdrive "test.log"
+
+ Set-Content $logpath -Value "hello"
+
+ $f = [System.IO.FileStream]::new($logpath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+
+ Add-Content $logpath -Value "world"
+
+ $f.Close()
+
+ $content = Get-Content $logpath
+ $content | Should -HaveCount 2
+ $content[0] | Should -BeExactly "hello"
+ $content[1] | Should -BeExactly "world"
+ }
+ }
+ }
\ No newline at end of file
diff --git a/Tests/PSProvider/Clear-Content.Tests.ps1 b/Tests/PSProvider/Clear-Content.Tests.ps1
new file mode 100644
index 0000000..266d4e7
--- /dev/null
+++ b/Tests/PSProvider/Clear-Content.Tests.ps1
@@ -0,0 +1,116 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# get a random string of characters a-z and A-Z
+function Get-RandomString
+{
+ param ( [int]$Length = 8 )
+ $chars = .{ ([int][char]'a')..([int][char]'z');([int][char]'A')..([int][char]'Z') }
+ ([char[]]($chars | Get-Random -Count $Length)) -join ""
+}
+
+# get a random string which is not the name of an existing provider
+function Get-NonExistantProviderName
+{
+ param ( [int]$Length = 8 )
+ do {
+ $providerName = Get-RandomString -Length $Length
+ } until ( $null -eq (Get-PSProvider -PSProvider $providername -ErrorAction SilentlyContinue) )
+ $providerName
+}
+
+# get a random string which is not the name of an existing drive
+function Get-NonExistantDriveName
+{
+ param ( [int]$Length = 8 )
+ do {
+ $driveName = Get-RandomString -Length $Length
+ } until ( $null -eq (Get-PSDrive $driveName -ErrorAction SilentlyContinue) )
+ $drivename
+}
+
+# get a random string which is not the name of an existing function
+function Get-NonExistantFunctionName
+{
+ param ( [int]$Length = 8 )
+ do {
+ $functionName = Get-RandomString -Length $Length
+ } until ( (Test-Path -Path function:$functionName) -eq $false )
+ $functionName
+}
+
+Describe "Clear-Content cmdlet tests" -Tags "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name TestDrive -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $file1 = "file1.txt"
+ $file2 = "file2.txt"
+ $file3 = "file3.txt"
+ $content1 = "This is content"
+ $content2 = "This is content for alternate stream tests"
+
+
+ New-Item -Path "TestDrive:\$file1" -ItemType File -Force
+ New-Item -Path "TestDrive:\$file2" -ItemType File -Value $content1 -Force
+ New-Item -Path "TestDrive:\$file3" -ItemType File -Value $content2 -Force
+
+ $streamContent = "content for alternate stream"
+ $streamName = "altStream1"
+ }
+
+ Context "Clear-Content should actually clear content" {
+ It "should clear-Content of TestDrive:\$file1" {
+ Set-Content -Path TestDrive:\$file1 -Value "ExpectedContent" -PassThru | Should -BeExactly "ExpectedContent"
+ Clear-Content -Path TestDrive:\$file1
+ }
+
+ It "shouldn't get any content from TestDrive:\$file1" {
+ $result = Get-Content -Path TestDrive:\$file1
+ $result | Should -BeNullOrEmpty
+ }
+
+ # we could suppress the WhatIf output here if we use the testhost, but it's not necessary
+ It "The filesystem provider supports should process" -skip:(!$IsWindows) {
+ Clear-Content -Path TestDrive:\$file2 -WhatIf
+ Get-Content -Path "TestDrive:\$file2" | Should -be "This is content"
+ }
+
+ It "The filesystem provider should support ShouldProcess (reference ProviderSupportsShouldProcess member)" {
+ $cci = ((Get-Command -Name Clear-Content).ImplementingType)::new()
+ $cci.SupportsShouldProcess | Should -BeTrue
+ }
+
+ }
+ Context "Proper errors should be delivered when bad locations are specified" {
+ It "should throw when targetting a directory." {
+ { Clear-Content -Path . -ErrorAction Stop } | Should -Throw -ErrorId "ClearDirectoryContent"
+ }
+
+ It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$null" {
+ { Clear-Content -Path $null -ErrorAction Stop } |
+ Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ClearContentCommand"
+ }
+
+ #[BugId(BugDatabase.WindowsOutOfBandReleases, 903880)]
+ It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$()" {
+ { Clear-Content -Path $() -ErrorAction Stop } |
+ Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ClearContentCommand"
+ }
+
+ #[DRT][BugId(BugDatabase.WindowsOutOfBandReleases, 906022)]
+ It "should throw 'PSNotSupportedException' when you clear-content to an unsupported provider" {
+ $functionName = Get-NonExistantFunctionName
+ $null = New-Item -Path function:$functionName -Value { 1 }
+ { Clear-Content -Path function:$functionName -ErrorAction Stop } |
+ Should -Throw -ErrorId "NotSupported,Microsoft.PowerShell.Commands.ClearContentCommand"
+ }
+
+ It "should throw FileNotFound error when referencing a non-existant file" {
+ $badFile = "TestDrive:/badfilename.txt"
+ { Clear-Content -Path $badFile -ErrorAction Stop } |
+ Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.ClearContentCommand"
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PSProvider/Get-Content.Tests.ps1 b/Tests/PSProvider/Get-Content.Tests.ps1
new file mode 100644
index 0000000..44e3ed2
--- /dev/null
+++ b/Tests/PSProvider/Get-Content.Tests.ps1
@@ -0,0 +1,415 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+Describe "Get-Content" -Tags "CI" {
+
+
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name PSProvider -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $TestDrive = "PSProvider:\"
+
+ $testString = "This is a test content for a file"
+ $nl = [Environment]::NewLine
+ $firstline = "Here's a first line "
+ $secondline = " here's a second line"
+ $thirdline = "more text"
+ $fourthline = "just to make sure"
+ $fifthline = "there's plenty to work with"
+ $testString2 = $firstline + $nl + $secondline + $nl + $thirdline + $nl + $fourthline + $nl + $fifthline
+ $testPath = Join-Path -Path $TestDrive -ChildPath testfile1
+ $testPath2 = Join-Path -Path $TestDrive -ChildPath testfile2
+ $testContent = "AA","BB","CC","DD"
+ $testDelimiterContent = "Hello1,World1","Hello2,World2","Hello3,World3","Hello4,World4"
+
+
+
+ #$ArchiveFile = "$PSScriptRoot\Tests\ZipFile"
+ #Import-Module .\Source\PS1C\bin\Debug\netcoreapp3.0\ps1c.dll
+ #New-PSDrive -Name PSProvider -PSProvider ZipFile -root "$ArchiveFile.zip" -ErrorAction "Stop"
+
+ function Out-PesterMessage {
+ param (
+ [int] $indent = 2,
+ [Parameter(ValueFromPipeline)]
+ [object] $InputObject
+ )
+ begin {
+ $InputObjects = New-Object "System.Collections.Generic.List[object]"
+ }
+ process {
+ # Collect all objects in Pipeline.
+ $InputObjects.Add($InputObject)
+ }
+ end {
+ $OutputString = $InputObjects |
+ Out-String |
+ ForEach-Object Trim |
+ ForEach-Object Split "`n" |
+ ForEach-Object { "{0}{1}" -f (" " * 4 * $indent), $_ } |
+ Write-Host -ForegroundColor Cyan
+ }
+ }
+
+ }
+
+ BeforeEach {
+ New-Item -Path $testPath -ItemType file -Force -Value $testString
+ New-Item -Path $testPath2 -ItemType file -Force -Value $testString2
+ }
+
+ AfterEach {
+ Remove-Item -Path $testPath -Force
+ Remove-Item -Path $testPath2 -Force
+ }
+
+ It "Should throw an error on a directory" {
+ { Get-Content . -ErrorAction Stop } |
+ Should -Throw -ErrorId "GetContainerContentException,Microsoft.PowerShell.Commands.GetContentCommand"
+ }
+
+ It "Should return an Object when listing only a single line and the correct information from a file" {
+ $content = (Get-Content -Path $testPath)
+ $content | Should -BeExactly $testString
+ $content.Count | Should -Be 1
+ $content | Should -BeOfType "System.String"
+ }
+
+ It "Should deliver an array object when listing a file with multiple lines and the correct information from a file" {
+ $content = (Get-Content -Path $testPath2)
+ @(Compare-Object $content $testString2.Split($nl) -SyncWindow 0).Length | Should -Be 0
+ ,$content | Should -BeOfType "System.Array"
+ }
+
+ It "Should be able to return a specific line from a file" {
+ (Get-Content -Path $testPath2)[1] | Should -BeExactly $secondline
+ }
+
+ It "Should be able to specify the number of lines to get the content of using the TotalCount switch" {
+ $returnArray = (Get-Content -Path $testPath2 -TotalCount 2)
+ $returnArray[0] | Should -BeExactly $firstline
+ $returnArray[1] | Should -BeExactly $secondline
+ }
+
+ It "Should be able to specify the number of lines to get the content of using the Head switch" {
+ $returnArray = (Get-Content -Path $testPath2 -Head 2)
+ $returnArray[0] | Should -BeExactly $firstline
+ $returnArray[1] | Should -BeExactly $secondline
+ }
+
+ It "Should be able to specify the number of lines to get the content of using the First switch" {
+ $returnArray = (Get-Content -Path $testPath2 -First 2)
+ $returnArray[0] | Should -BeExactly $firstline
+ $returnArray[1] | Should -BeExactly $secondline
+ }
+
+ It "Should return the last line of a file using the Tail switch" {
+ # Get-Content -Path $testPath -Tail 1 | Should -BeExactly $testString
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ }
+
+ It "Should return the last lines of a file using the Last alias" {
+ # Get-Content -Path $testPath2 -Last 1 | Should -BeExactly $fifthline
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+
+ }
+
+ # It "Should be able to get content within a different drive" {
+ # Push-Location env:
+ # $expectedoutput = [Environment]::GetEnvironmentVariable("PATH");
+ # { Get-Content PATH } | Should -Not -Throw
+ # Get-Content PATH | Should -BeExactly $expectedoutput
+ # Pop-Location
+ # }
+
+ # [BugId(BugDatabase.WindowsOutOfBandReleases, 906022)]
+ # It "should throw 'PSNotSupportedException' when you Set-Content to an unsupported provider" -Skip:($IsLinux -Or $IsMacOS) {
+ # {Get-Content -Path HKLM:\\software\\microsoft -ErrorAction Stop} | Should -Throw "IContentCmdletProvider interface is not implemented"
+ # }
+
+ # It 'Verifies -Tail reports a TailNotSupported error for unsupported providers' {
+ # {Get-Content -Path Variable:\PSHOME -Tail 1 -ErrorAction Stop} | Should -Throw -ErrorId 'TailNotSupported,Microsoft.PowerShell.Commands.GetContentCommand'
+ # }
+
+ It 'Verifies using -Tail and -TotalCount together reports a TailAndHeadCannotCoexist error' {
+ # { Get-Content -Path Variable:\PSHOME -Tail 1 -TotalCount 5 -ErrorAction Stop} | Should -Throw -ErrorId 'TailAndHeadCannotCoexist,Microsoft.PowerShell.Commands.GetContentCommand'
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ }
+
+ It 'Verifies -Tail with content that uses an explicit encoding' -TestCases @(
+ @{EncodingName = 'String'},
+ @{EncodingName = 'Unicode'},
+ @{EncodingName = 'BigEndianUnicode'},
+ @{EncodingName = 'UTF8'},
+ @{EncodingName = 'UTF7'},
+ @{EncodingName = 'UTF32'},
+ @{EncodingName = 'Ascii'}
+ ){
+ param($EncodingName)
+
+ $content = @"
+one
+two
+foo
+bar
+baz
+"@
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+
+ # $expected = 'foo'
+ # $tailCount = 3
+
+ # $testPath = Join-Path -Path $TestDrive -ChildPath 'TailWithEncoding.txt'
+ # $content | Set-Content -Path $testPath -Encoding $encodingName
+ # $expected = 'foo'
+
+ # $actual = Get-Content -Path $testPath -Tail $tailCount -Encoding $encodingName
+ # $actual | Should -BeOfType [string]
+ # $actual.Length | Should -Be $tailCount
+ # $actual[0] | Should -BeExactly $expected
+ }
+
+ It "should Get-Content with a variety of -Tail and -ReadCount: " -TestCases @(
+ @{ test = "negative readcount"
+ GetContentParams = @{Path = $testPath; Readcount = -1; Tail = 5}
+ expectedLength = 4
+ expectedContent = "AA","BB","CC","DD"
+ }
+ @{ test = "readcount=0"
+ GetContentParams = @{Path = $testPath; Readcount = 0; Tail = 3}
+ expectedLength = 3
+ expectedContent = "BB","CC","DD"
+ }
+ @{ test = "readcount=1"
+ GetContentParams = @{Path = $testPath; Readcount = 1; Tail = 3}
+ expectedLength = 3
+ expectedContent = "BB","CC","DD"
+ }
+ @{ test = "high readcount"
+ GetContentParams = @{Path = $testPath; Readcount = 99999; Tail = 3}
+ expectedLength = 3
+ expectedContent = "BB","CC","DD"
+ }
+ @{ test = "readcount=2 tail=3"
+ GetContentParams = @{Path = $testPath; Readcount = 2; Tail = 3}
+ expectedLength = 2
+ expectedContent = ("BB","CC"), "DD"
+ }
+ @{ test = "readcount=2 tail=2"
+ GetContentParams = @{Path = $testPath; Readcount = 2; Tail = 2}
+ expectedLength = 2
+ expectedContent = "CC","DD"
+ }
+ ) {
+ param($GetContentParams, $expectedLength, $expectedContent)
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # Set-Content -Path $testPath $testContent
+ # $result = Get-Content @GetContentParams
+ # $result.Length | Should -Be $expectedLength
+ # $result | Should -BeExactly $expectedContent
+ }
+
+ It "should Get-Content with a variety of -Delimiter and -Tail: " -TestCases @(
+ @{ test = ", as delimiter"
+ GetContentParams = @{Path = $testPath; Delimiter = ","; Tail = 2}
+ expectedLength = 2
+ expectedContent = "World3${nl}Hello4", "World4${nl}"
+ }
+ @{ test = "o as delimiter"
+ GetContentParams = @{Path = $testPath; Delimiter = "o"; Tail = 3}
+ expectedLength = 3
+ expectedContent = "rld3${nl}Hell", '4,W', "rld4${nl}"
+ }
+ ) {
+ param($GetContentParams, $expectedLength, $expectedContent)
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+
+ # Set-Content -Path $testPath $testDelimiterContent
+ # $result = Get-Content @GetContentParams
+ # $result.Length | Should -Be $expectedLength
+ # $result | Should -BeExactly $expectedContent
+ }
+
+ It "should Get-Content with a variety of -Tail values and -AsByteStream parameter" -TestCases @(
+ @{
+ GetContentParams = @{
+ Path = $testPath;
+ Tail = 10;
+ # TotalCount = 10;
+ AsByteStream = $true
+ }
+ expectedLength = 10
+ # Byte encoding of \r\nCC\r\nDD\r\n
+ expectedWindowsContent = 13, 10, 67, 67, 13, 10, 68, 68, 13, 10
+ # Byte encoding of \nBB\nCC\nDD\n
+ expectedNotWindowsContent = 10, 66, 66, 10, 67, 67, 10, 68, 68, 10
+ }
+ ) {
+ param($GetContentParams, $expectedLength, $expectedWindowsContent, $expectedNotWindowsContent)
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+
+ # Set-Content -Path $testPath $testContent
+ # $result = Get-Content @GetContentParams
+ # $result.Length | Should -Be $expectedLength
+ # if ($isWindows) {
+ # $result | Should -BeExactly $expectedWindowsContent
+ # } else {
+ # $result | Should -BeExactly $expectedNotWindowsContent
+ # }
+ }
+
+
+ #[BugId(BugDatabase.WindowsOutOfBandReleases, 905829)]
+ It "should Get-Content that matches the input string"{
+ Set-Content $testPath "Hello,llllWorlld","Hello2,llllWorlld2"
+ $result = Get-Content $testPath -Delimiter "ll"
+ $result.Length | Should -Be 9
+
+ $expected = 'He', 'o,', '', 'Wor', "d${nl}He", 'o2,', '', 'Wor', "d2${nl}"
+ for ($i = 0; $i -lt $result.Length ; $i++) { $result[$i] | Should -BeExactly $expected[$i]}
+ }
+
+ # It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) {
+ # Set-Content "${testPath}:Stream" -Value "Foo"
+ # { Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" }
+ # Get-Content "${testPath}:Stream" | Should -BeExactly "Foo"
+ # Get-Content $testPath | Should -BeExactly $testString
+ # }
+
+ # It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) {
+ # Set-Content -Path $testPath -Stream hello -Value World
+ # Get-Content -Path $testPath | Should -BeExactly $testString
+ # Get-Content -Path $testPath -Stream hello | Should -BeExactly "World"
+ # $item = Get-Item -Path $testPath -Stream hello
+ # $item | Should -BeOfType 'System.Management.Automation.Internal.AlternateStreamData'
+ # $item.Stream | Should -BeExactly "hello"
+ # Clear-Content -Path $testPath -Stream hello
+ # Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty
+ # Remove-Item -Path $testPath -Stream hello
+ # { Get-Content -Path $testPath -Stream hello | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" }
+ # }
+
+ # It "Should support colons in filename on Linux/Mac" -Skip:($IsWindows) {
+ # Set-Content "${testPath}:Stream" -Value "Hello"
+ # "${testPath}:Stream" | Should -Exist
+ # Get-Content "${testPath}:Stream" | Should -BeExactly "Hello"
+ # }
+
+ # It "-Stream is not a valid parameter for on Linux/Mac" -Skip:($IsWindows) -TestCases @(
+ # @{cmdlet="Get-Content"},
+ # @{cmdlet="Set-Content"},
+ # @{cmdlet="Clear-Content"},
+ # @{cmdlet="Add-Content"},
+ # @{cmdlet="Get-Item"},
+ # @{cmdlet="Remove-Item"}
+ # ) {
+ # param($cmdlet)
+ # (Get-Command $cmdlet).Parameters["stream"] | Should -BeNullOrEmpty
+ # }
+
+ It "Should return no content when an empty path is used with -Raw switch" {
+ Set-ItResult -Inconclusive -Because "TODO: Get-ChildItem is failing due to not implimented yet"
+ Get-ChildItem $TestDrive -Filter "*.raw" | Get-Content -Raw | Should -BeNullOrEmpty
+ }
+
+ It "Should return no content when -TotalCount value is 0" {
+ Get-Content -Path $testPath -TotalCount 0 | Should -BeNullOrEmpty
+ }
+
+
+ It "Should throw TailAndHeadCannotCoexist when both -Tail and -TotalCount are used" {
+ {
+ Get-Content -Path $testPath -Tail 1 -TotalCount 1 -ErrorAction Stop
+ } | Should -Throw -ErrorId "TailAndHeadCannotCoexist,Microsoft.PowerShell.Commands.GetContentCommand"
+ }
+
+ It "Should throw InvalidOperation when -Tail and -Raw are used" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # {
+ # Get-Content -Path $testPath -Tail 1 -ErrorAction Stop -Raw
+ # } | Should -Throw -ErrorId "InvalidOperation,Microsoft.PowerShell.Commands.GetContentCommand"
+ }
+
+ It "Should throw ItemNotFound when path matches no files with " -TestCases @(
+ @{ variation = "no additional parameters"; params = @{} },
+ @{ variation = "dynamic parameter" ; params = @{ Raw = $true }}
+ ) {
+ param($params)
+
+ { Get-Content -Path "/DoesNotExist*.txt" @params -ErrorAction Stop } | Should -Throw -ErrorId "ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand"
+ }
+ Context "Check Get-Content containing multi-byte chars" {
+ BeforeAll {
+ $firstLine = "Hello,World"
+ $secondLine = "Hello2,World2"
+ $thirdLine = "Hello3,World3"
+ $fourthLine = "Hello4,World4"
+ $fileContent = $firstLine,$secondLine,$thirdLine,$fourthLine
+ }
+ BeforeEach {
+ Set-Content -Path $testPath $fileContent
+ }
+
+ It "Should return all lines when -Tail value is more than number of lines in the file" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # $result = Get-Content -Path $testPath -ReadCount -1 -Tail 5 -Encoding UTF7
+ # $result.Length | Should -Be 4
+ # $expected = $fileContent
+ # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "Should return last three lines at one time for -ReadCount 0 and -Tail 3" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # $result = Get-Content -Path $testPath -ReadCount 0 -Tail 3 -Encoding UTF7
+ # $result.Length | Should -Be 3
+ # $expected = $secondLine,$thirdLine,$fourthLine
+ # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "Should return last three lines reading one at a time for -ReadCount 1 and -Tail 3" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # $result = Get-Content -Path $testPath -ReadCount 1 -Tail 3 -Encoding UTF7
+ # $result.Length | Should -Be 3
+ # $expected = $secondLine,$thirdLine,$fourthLine
+ # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "Should return last three lines at one time for -ReadCount 99999 and -Tail 3" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # $result = Get-Content -Path $testPath -ReadCount 99999 -Tail 3 -Encoding UTF7
+ # $result.Length | Should -Be 3
+ # $expected = $secondLine,$thirdLine,$fourthLine
+ # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "Should return last three lines two lines at a time for -ReadCount 2 and -Tail 3" {
+ Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers"
+ # $result = Get-Content -Path $testPath -ReadCount 2 -Tail 3 -Encoding UTF7
+ # $result.Length | Should -Be 2
+ # $expected = New-Object System.Array[] 2
+ # $expected[0] = ($secondLine,$thirdLine)
+ # $expected[1] = $fourthLine
+ # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "Should not return any content when -TotalCount 0" {
+ $result = Get-Content -Path $testPath -TotalCount 0 -ReadCount 1 -Encoding UTF7
+ $result.Length | Should -Be 0
+ }
+
+ It "Should return first three lines two lines at a time for -TotalCount 3 and -ReadCount 2" {
+ $result = Get-Content -Path $testPath -TotalCount 3 -ReadCount 2 -Encoding UTF7
+ $result.Length | Should -Be 2
+ $expected = New-Object System.Array[] 2
+ $expected[0] = ($firstLine,$secondLine)
+ $expected[1] = $thirdLine
+ Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty
+ }
+
+ It "A warning should be emitted if both -AsByteStream and -Encoding are used together" {
+ [byte[]][char[]]"test" | Set-Content -Encoding Unicode -AsByteStream -Path "${TESTDRIVE}\bfile.txt" -WarningVariable contentWarning *> $null
+ $contentWarning.Message | Should -Match "-AsByteStream"
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Tests/PSProvider/New-Item.Tests.ps1 b/Tests/PSProvider/New-Item.Tests.ps1
new file mode 100644
index 0000000..ef9ad2d
--- /dev/null
+++ b/Tests/PSProvider/New-Item.Tests.ps1
@@ -0,0 +1,225 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+Describe "New-Item" -Tags "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name TestDrive -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $TestDrive = "TestDrive:\"
+ $tmpDirectory = $TestDrive
+ $testfile = "testfile.txt"
+ $testfolder = "newDirectory"
+ $testsubfolder = "newSubDirectory"
+ $testlink = "testlink"
+ $FullyQualifiedFile = Join-Path -Path $tmpDirectory -ChildPath $testfile
+ $FullyQualifiedFolder = Join-Path -Path $tmpDirectory -ChildPath $testfolder
+ $FullyQualifiedLink = Join-Path -Path $tmpDirectory -ChildPath $testlink
+ $FullyQualifiedSubFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testsubfolder
+ $FullyQualifiedFileInFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testfile
+
+ }
+
+ BeforeEach {
+ if (Test-Path $FullyQualifiedLink)
+ {
+ Remove-Item $FullyQualifiedLink -Force
+ }
+
+ if (Test-Path $FullyQualifiedFile)
+ {
+ Remove-Item $FullyQualifiedFile -Force
+ }
+
+ if ($FullyQualifiedFileInFolder -and (Test-Path $FullyQualifiedFileInFolder))
+ {
+ Remove-Item $FullyQualifiedFileInFolder -Force
+ }
+
+ if ($FullyQualifiedSubFolder -and (Test-Path $FullyQualifiedSubFolder))
+ {
+ Remove-Item $FullyQualifiedSubFolder -Force
+ }
+
+ if (Test-Path $FullyQualifiedFolder)
+ {
+ Remove-Item $FullyQualifiedFolder -Force
+ }
+
+ }
+
+ It "should call the function without error" {
+ { New-Item -Name $testfile -Path $tmpDirectory -ItemType file } | Should -Not -Throw
+ }
+
+ It "should call the function without error" {
+ { New-Item -Name $testfile -Path $tmpDirectory -ItemType file } | Should -Not -Throw
+ }
+
+ It "Should create a file without error" {
+ New-Item -Name $testfile -Path $tmpDirectory -ItemType file
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+
+ $fileInfo = Get-ChildItem $FullyQualifiedFile
+ $fileInfo.Target | Should -BeNullOrEmpty
+ $fileInfo.LinkType | Should -BeNullOrEmpty
+ }
+
+ It "Should create a folder without an error" {
+ New-Item -Name newDirectory -Path $tmpDirectory -ItemType directory
+ Test-Path $FullyQualifiedFolder | Should -BeTrue
+ }
+
+ It "Should create a file using the ni alias" {
+ ni -Name $testfile -Path $tmpDirectory -ItemType file
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ }
+
+ It "Should create a file using the Type alias instead of ItemType" {
+ New-Item -Name $testfile -Path $tmpDirectory -Type file
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ }
+
+ It "Should create a file with sample text inside the file using the Value switch" {
+ $expected = "This is test string"
+ New-Item -Name $testfile -Path $tmpDirectory -ItemType file -Value $expected
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+
+ Get-Content $FullyQualifiedFile | Should -Be $expected
+ }
+
+ It "Should not create a file when the Name switch is not used and only a directory specified" {
+ #errorAction used because permissions issue in Windows
+
+ New-Item -Path $tmpDirectory -ItemType file -ErrorAction SilentlyContinue
+ Test-Path $FullyQualifiedFile | Should -BeFalse
+ }
+
+ It "Should create a file when the Name switch is not used but a fully qualified path is specified" {
+ New-Item -Path $FullyQualifiedFile -ItemType file
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ }
+
+ It "Should be able to create a multiple items in different directories" {
+ $FullyQualifiedFile2 = Join-Path -Path $tmpDirectory -ChildPath test2.txt
+ New-Item -ItemType file -Path $FullyQualifiedFile, $FullyQualifiedFile2
+
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ Test-Path $FullyQualifiedFile2 | Should -BeTrue
+
+ Remove-Item $FullyQualifiedFile2
+ }
+
+ It "Should be able to call the whatif switch without error" {
+ { New-Item -Name testfile.txt -Path $tmpDirectory -ItemType file -WhatIf } | Should -Not -Throw
+ }
+
+ It "Should not create a new file when the whatif switch is used" {
+ New-Item -Name $testfile -Path $tmpDirectory -ItemType file -WhatIf
+
+ Test-Path $FullyQualifiedFile | Should -BeFalse
+ }
+
+ It "Should create a file at the root of the drive while the current working directory is not the root" {
+ try {
+ New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null
+ Push-Location -Path "TestDrive:\$testfolder"
+ New-Item -Name $testfile -Path "TestDrive:\" -ItemType file > $null
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ #Code changed pester for some odd reason dosnt like Should -Exist
+ #$FullyQualifiedFile | Should -Exist
+ }
+ finally {
+ Pop-Location
+
+ }
+ }
+
+ It "Should create a folder at the root of the drive while the current working directory is not the root" {
+ $testfolder2 = "newDirectory2"
+ $FullyQualifiedFolder2 = Join-Path -Path $tmpDirectory -ChildPath $testfolder2
+
+ try {
+ New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null
+ Push-Location -Path "TestDrive:\$testfolder"
+ New-Item -Name $testfolder2 -Path "TestDrive:\" -ItemType directory > $null
+ Test-Path $FullyQualifiedFolder2 | Should -BeTrue
+ #Code changed pester for some odd reason dosnt like Should -Exist
+ #$FullyQualifiedFolder2 | Should -Exist
+
+ }
+ finally {
+ Pop-Location
+
+ #Fixed a bug where cleanup wasnt happening
+ Remove-Item $FullyQualifiedFolder2 -Force
+
+ }
+ }
+
+ It "Should create a file in the current directory when using Drive: notation" {
+ try {
+ New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null
+ Push-Location -Path "TestDrive:\$testfolder"
+ New-Item -Name $testfile -Path "TestDrive:" -ItemType file > $null
+
+ Test-Path $FullyQualifiedFileInFolder | Should -BeTrue
+ #Code changed pester for some odd reason dosnt like Should -Exist
+ #$FullyQualifiedFileInFolder | Should -Exist
+ }
+ finally {
+ Pop-Location
+ }
+ }
+
+ It "Should create a folder in the current directory when using Drive: notation" {
+ try {
+ New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null
+ Push-Location -Path "TestDrive:\$testfolder"
+ New-Item -Name $testsubfolder -Path "TestDrive:" -ItemType file > $null
+ Test-Path $FullyQualifiedSubFolder | Should -BeTrue
+ #Code changed pester for some odd reason dosnt like Should -Exist
+ #$FullyQualifiedSubFolder | Should -Exist
+ }
+ finally {
+ Pop-Location
+ }
+ }
+}
+
+# More precisely these tests require SeCreateSymbolicLinkPrivilege.
+# You can see list of priveledges with `whoami /priv`.
+# In the default windows setup, Admin user has this priveledge, but regular users don't.
+
+Describe "New-Item -Force allows to create an item even if the directories in the path don't exist" -Tags "CI" {
+ BeforeAll {
+ $TestDrive = "TestDrive:\"
+ $testFile = 'testfile.txt'
+ $testFolder = 'testfolder'
+ $FullyQualifiedFolder = Join-Path -Path $TestDrive -ChildPath $testFolder
+ $FullyQualifiedFile = Join-Path -Path $TestDrive -ChildPath $testFolder -AdditionalChildPath $testFile
+ }
+
+ BeforeEach {
+ # Explicitly removing folder and the file before tests
+ Remove-Item $FullyQualifiedFolder -Recurse -ErrorAction SilentlyContinue
+ Remove-Item $FullyQualifiedFile -Recurse -ErrorAction SilentlyContinue
+ Test-Path -Path $FullyQualifiedFolder | Should -BeFalse
+ Test-Path -Path $FullyQualifiedFile | Should -BeFalse
+ }
+
+ It "Should error correctly when -Force is not used and folder in the path doesn't exist" {
+ { New-Item $FullyQualifiedFile -ErrorAction Stop } | Should -Throw -ErrorId 'NewItemIOError,Microsoft.PowerShell.Commands.NewItemCommand'
+ Test-Path $FullyQualifiedFile | Should -BeFalse
+ }
+ It "Should create new file correctly when -Force is used and folder in the path doesn't exist" {
+ { New-Item $FullyQualifiedFile -Force -ErrorAction Stop } | Should -Not -Throw
+ Test-Path $FullyQualifiedFile | Should -BeTrue
+ }
+}
+
diff --git a/Tests/PSProvider/Remove-Item.Tests.ps1 b/Tests/PSProvider/Remove-Item.Tests.ps1
new file mode 100644
index 0000000..c2f8e9e
--- /dev/null
+++ b/Tests/PSProvider/Remove-Item.Tests.ps1
@@ -0,0 +1,137 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+Describe "Remove-Item" -Tags "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name TestDrive -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $TestDrive = "TestDrive:\"
+ $testpath = $TestDrive
+ $testfile = "testfile.txt"
+ $testfilepath = Join-Path -Path $testpath -ChildPath $testfile
+ }
+
+ AfterAll {
+
+ }
+ Context "File removal Tests" {
+ BeforeEach {
+ New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+
+ Test-Path $testfilepath | Should -BeTrue
+ }
+
+ It "Should be able to be called on a regular file without error using the Path parameter" {
+ { Remove-Item -Path $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+ It "Should be able to be called on a file without the Path parameter" {
+ { Remove-Item $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to call the rm alias" -Skip:($IsLinux -Or $IsMacOS) {
+ { rm $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to call the del alias" {
+ { del $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to call the erase alias" {
+ { erase $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to call the ri alias" {
+ { ri $testfilepath } | Should -Not -Throw
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to remove all files matching a regular expression with the include parameter" {
+ # Create multiple files with specific string
+ New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+ New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+ New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+ # Create a single file that does not match that string - already done in BeforeEach
+
+ # Delete the specific string
+ Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt
+ # validate that the string under test was deleted, and the nonmatching strings still exist
+ Test-path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
+ Test-path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse
+ Test-path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse
+ Test-Path $testfilepath | Should -BeTrue
+
+ # Delete the non-matching strings
+
+ Remove-Item $testfilepath -Recurse
+
+ Test-Path $testfilepath | Should -BeFalse
+ }
+
+ It "Should be able to not remove any files matching a regular expression with the exclude parameter" {
+ # Create multiple files with specific string
+ New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+ New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+
+ # Create a single file that does not match that string
+ New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
+
+ # Delete the specific string
+ Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt
+
+ # validate that the string under test was deleted, and the nonmatching strings still exist
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue
+ Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
+
+ # Delete the non-matching strings
+ Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav)
+ Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav)
+
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse
+ Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse
+ }
+ }
+
+ Context "Directory Removal Tests" {
+ BeforeAll {
+ $testdirectory = Join-Path -Path $testpath -ChildPath testdir
+ $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd
+ }
+
+ BeforeEach {
+ New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force
+
+ Test-Path $testdirectory | Should -BeTrue
+ }
+
+ It "Should be able to remove a directory" {
+ { Remove-Item $testdirectory } | Should -Not -Throw
+
+ Test-Path $testdirectory | Should -BeFalse
+ }
+
+ It "Should be able to recursively delete subfolders" {
+ New-Item -Name "subd" -Path $testdirectory -ItemType "directory"
+ New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum"
+
+ $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile
+ test-path $complexDirectory | Should -BeTrue
+
+ { Remove-Item $testdirectory -Recurse} | Should -Not -Throw
+
+ Test-Path $testdirectory | Should -BeFalse
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PSProvider/Rename-Item.Tests.ps1 b/Tests/PSProvider/Rename-Item.Tests.ps1
new file mode 100644
index 0000000..dfe6181
--- /dev/null
+++ b/Tests/PSProvider/Rename-Item.Tests.ps1
@@ -0,0 +1,85 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+Describe "Rename-Item tests" -Tag "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name TestDrive -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $TestDrive = "TestDrive:"
+
+ $source = "$TESTDRIVE/originalFile.txt"
+ $target = "$TESTDRIVE/ItemWhichHasBeenRenamed.txt"
+
+
+ $sourceSp = "$TestDrive/``[orig-file``].txt"
+ $targetSpName = "ItemWhichHasBeen[Renamed].txt"
+ $targetSp = "$TestDrive/ItemWhichHasBeen``[Renamed``].txt"
+# Setup -Dir [test-dir]
+ $wdSp = "$TestDrive/``[test-dir``]"
+
+ # Setup file System
+ New-Item $Source -Value "This is content" -Force -ErrorAction Continue
+
+ try {
+ New-Item $sourceSP -Value "This is not content" -Force -ErrorAction Continue
+ New-Item "$wdSp" -ItemType Directory -ErrorAction Continue
+ }
+ catch {
+
+ }
+
+ }
+ AfterAll {
+ Write-Host "AfterAll"
+ try {
+ Remove-Item $target -Force -ErrorAction Continue
+ Remove-Item $targetSpName -Force -ErrorAction Continue
+ #Remove-Item $targetSp -Force -ErrorAction Continue
+ }
+ catch {}
+ }
+ It "Rename-Item will rename a file" {
+ Rename-Item $source $target
+
+ test-path $source | Should -BeFalse
+ test-path $target | Should -BeTrue
+
+ Get-Content $target | should -Be "This is content"
+ }
+ It "Rename-Item will rename a file when path contains special char" {
+ Rename-Item $sourceSp $targetSpName
+ test-path $sourceSp | Should -BeFalse
+ #test-path $targetSp | Should -true
+ #Get-Content $targetSp | should -Be "This is content"
+ }
+# It "Rename-Item will rename a file when -Path and CWD contains special char" {
+# $content = "This is content"
+# $oldSpName = "[orig]file.txt"
+# $oldSpBName = "``[orig``]file.txt"
+# $oldSp = "$wdSp/$oldSpBName"
+# $newSpName = "[renamed]file.txt"
+# $newSp = "$wdSp/``[renamed``]file.txt"
+# In $wdSp -Execute {
+# $null = New-Item -Name $oldSpName -ItemType File -Value $content -Force
+# Rename-Item -Path $oldSpBName $newSpName
+# }
+# $oldSp | Should -Not -Exist
+# $newSp | Should -Exist
+# $newSp | Should -FileContentMatchExactly $content
+# }
+# It "Rename-Item will rename a file when -LiteralPath and CWD contains special char" {
+# $content = "This is not content"
+# $oldSpName = "[orig]file2.txt"
+# $oldSpBName = "``[orig``]file2.txt"
+# $oldSp = "$wdSp/$oldSpBName"
+# $newSpName = "[renamed]file2.txt"
+# $newSp = "$wdSp/``[renamed``]file2.txt"
+# In $wdSp -Execute {
+# $null = New-Item -Name $oldSpName -ItemType File -Value $content -Force
+# Rename-Item -LiteralPath $oldSpName $newSpName
+# }
+# $oldSp | Should -Not -Exist
+# $newSp | Should -Exist
+# $newSp | Should -FileContentMatchExactly $content
+# }
+}
\ No newline at end of file
diff --git a/Tests/PSProvider/Set-Content.Tests.ps1 b/Tests/PSProvider/Set-Content.Tests.ps1
new file mode 100644
index 0000000..6e42ac7
--- /dev/null
+++ b/Tests/PSProvider/Set-Content.Tests.ps1
@@ -0,0 +1,78 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+Describe "Set-Content cmdlet tests" -Tags "CI" {
+ BeforeAll {
+ Import-Module "$PSScriptRoot\..\..\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psd1"
+ New-PSDrive -Name PSProvider -PSProvider Microsoft.PowerShell.Archive -root "$PSScriptRoot/ZipFile.Zip" -ErrorAction "Stop"
+
+ $testdrive = "PSProvider:\"
+
+ $file1 = "file1.txt"
+ $filePath1 = Join-Path $testdrive $file1
+
+ try { New-Item PSProvider:\bfile.txt -Value "" -ErrorAction Continue }
+ catch {}
+ }
+
+ It "A warning should be emitted if both -AsByteStream and -Encoding are used together" {
+ $testfile = "${TESTDRIVE}\bfile.txt"
+ "test" | Set-Content $testfile
+ $result = Get-Content -AsByteStream -Encoding Unicode -Path $testfile -WarningVariable contentWarning *> $null
+ $contentWarning.Message | Should -Match "-AsByteStream"
+ }
+
+ Context "Set-Content should create a file if it does not exist" {
+ AfterEach {
+ Remove-Item -Path $filePath1 -Force -ErrorAction SilentlyContinue
+ }
+ It "should create a file if it does not exist" {
+ Set-Content -Path $filePath1 -Value "$file1"
+ $result = Get-Content -Path $filePath1
+ $result| Should -Be "$file1"
+ }
+ }
+ Context "Set-Content/Get-Content should set/get the content of an exisiting file" {
+ BeforeAll {
+ New-Item -Path $filePath1 -ItemType File -Force
+ }
+ It "should set-Content of testdrive\$file1" {
+ Set-Content -Path $filePath1 -Value "ExpectedContent"
+ $result = Get-Content -Path $filePath1
+ $result| Should -Be "ExpectedContent"
+ }
+ It "should return expected string from testdrive\$file1" {
+ $result = Get-Content -Path $filePath1
+ $result | Should -BeExactly "ExpectedContent"
+ }
+ It "should Set-Content to testdrive\dynamicfile.txt with dynamic parameters" {
+ Set-Content -Path $testdrive\dynamicfile.txt -Value "ExpectedContent"
+ $result = Get-Content -Path $testdrive\dynamicfile.txt
+ $result | Should -BeExactly "ExpectedContent"
+ }
+ It "should return expected string from testdrive\dynamicfile.txt" {
+ $result = Get-Content -Path $testdrive\dynamicfile.txt
+ $result | Should -BeExactly "ExpectedContent"
+ }
+ It "should remove existing content from testdrive\$file1 when the -Value is `$null" {
+ $AsItWas = Get-Content $filePath1
+ $AsItWas | Should -BeExactly "ExpectedContent"
+ Set-Content -Path $filePath1 -Value $null -ErrorAction Stop
+ $AsItIs = Get-Content $filePath1
+ $AsItIs | Should -Not -Be $AsItWas
+ }
+ It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$null" {
+ { Set-Content -Path $null -Value "ShouldNotWorkBecausePathIsNull" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SetContentCommand"
+ }
+ It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$()" {
+ { Set-Content -Path $() -Value "ShouldNotWorkBecausePathIsInvalid" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SetContentCommand"
+ }
+ #[BugId(BugDatabase.WindowsOutOfBandReleases, 9058182)]
+ It "should be able to pass multiple [string]`$objects to Set-Content through the pipeline to output a dynamic Path file" {
+ "hello","world"|Set-Content $testdrive\dynamicfile2.txt
+ $result=Get-Content $testdrive\dynamicfile2.txt
+ $result.length | Should -Be 2
+ $result[0] | Should -BeExactly "hello"
+ $result[1] | Should -BeExactly "world"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PSProvider/ZipFile.BadFile.zip b/Tests/PSProvider/ZipFile.BadFile.zip
new file mode 100644
index 0000000..e69de29
diff --git a/Tests/PSProvider/ZipFile.zip b/Tests/PSProvider/ZipFile.zip
new file mode 100644
index 0000000..e878457
Binary files /dev/null and b/Tests/PSProvider/ZipFile.zip differ
diff --git a/appveyor.yml b/appveyor.yml
index ac70956..1827c85 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -13,6 +13,7 @@ build: false
# Run Pester tests and store the results
test_script:
- ps: |
+ dotnet build
$testResultsFile = ".\ArchiveTestResults.xml"
Import-Module "C:\projects\Archive-Module\Microsoft.PowerShell.Archive" -Force
$testResults = Invoke-Pester -Script "C:\projects\Archive-Module\Tests" -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru
diff --git a/src/Microsoft.PowerShell.Archive/ArchiveContentStream.cs b/src/Microsoft.PowerShell.Archive/ArchiveContentStream.cs
new file mode 100644
index 0000000..4ced332
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/ArchiveContentStream.cs
@@ -0,0 +1,54 @@
+
+using Microsoft.PowerShell.Commands;
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Management.Automation;
+using System.Management.Automation.Provider;
+using System.Text;
+
+namespace Microsoft.PowerShell.Archive
+{
+ #region StreamContent
+
+ #region ArchiveContentStream
+ public class ArchiveContentStream : StreamContentReaderWriter
+ {
+
+ private ArchiveItemInfo _archiveFileInfo;
+ private ArchiveItemStream _archiveFileStream;
+
+ private ArchiveItemStream stream;
+ private CmdletProvider _provider;
+
+
+ public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream)
+ : base( archiveFileInfo.Open(mode), encoding, usingByteEncoding, provider, isRawStream)
+ {
+ _provider = provider;
+ }
+
+ public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream, bool suppressNewline)
+ : base(archiveFileInfo.Open(mode), encoding, usingByteEncoding, provider, isRawStream, suppressNewline)
+ {
+ _provider = provider;
+ }
+
+ public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, string delimiter, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream)
+ : base(archiveFileInfo.Open(mode), delimiter, encoding, provider, isRawStream)
+ {
+ _provider = provider;
+ }
+
+
+ ~ArchiveContentStream()
+ {
+
+ }
+
+ }
+ #endregion ArchiveContentStream
+
+ #endregion StreamContent
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/ArchiveItemInfo.cs b/src/Microsoft.PowerShell.Archive/ArchiveItemInfo.cs
new file mode 100644
index 0000000..b3d107b
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/ArchiveItemInfo.cs
@@ -0,0 +1,441 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Management.Automation;
+using System.Management.Automation.Internal;
+
+namespace Microsoft.PowerShell.Archive
+{
+ #region ArchiveItemInfo
+ public class ArchiveItemInfo
+ {
+ //Public Extension info
+
+ //public DateTime CreationTime; // {get;set;}
+ //public DateTime CreationTimeUtc; // {get;set;}
+ public ArchivePSDriveInfo Drive {
+ get;
+ private set;
+ }
+
+ public DirectoryInfo Directory; // {get;}
+
+ public string DirectoryName
+ {
+ get {
+ if (IsContainer)
+ {
+ return Path.GetDirectoryName(PathUtils.TrimEndingDirectorySeparator(FullName));
+ }
+
+ return Path.GetDirectoryName(FullName);
+ }
+ }
+
+ public bool Exists {
+ get {
+ return true;
+ }
+ }
+
+ public object Crc32 {
+ get {
+ return null; //archiveEntry.Crc32;
+ }
+ }
+
+ public string Extension {
+ get {
+ return Path.GetExtension(FullName);
+ }
+ }
+
+ public string BaseName {
+ get {
+ return Path.GetFileNameWithoutExtension(FullName);
+ }
+ }
+
+ public string FullName {
+ get {
+ return String.Format("{0}:\\{1}", Drive.Name, ArchiveEntry.FullName).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ }
+ }
+
+ public string FullArchiveName {
+ get {
+ return ArchiveEntry.FullName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get {
+ return false;
+ }
+ set {
+
+ }
+ }
+
+ //public DateTime LastAccessTime; // {get;set;}
+ //public DateTime LastAccessTimeUtc; // {get;set;}
+ public DateTime LastWriteTime
+ {
+ get {
+ return ArchiveEntry.LastWriteTime.DateTime;
+ }
+ set {
+ // Todo: Fix writetime so it updates the archive as well
+ ArchiveEntry.LastWriteTime = new DateTimeOffset(value);
+ }
+ }
+
+ public DateTime LastWriteTimeUtc
+ {
+ get {
+ return this.LastWriteTime.ToUniversalTime();
+ }
+ set {
+ this.LastWriteTime = value.ToLocalTime();
+ }
+ }
+
+ public long Length {
+ get {
+ return ArchiveEntry.Length;
+ }
+ }
+
+ public long CompressedLength {
+ get {
+ return ArchiveEntry.CompressedLength;
+ }
+ }
+
+ public string Name {
+ get {
+ if (IsContainer)
+ {
+ return Path.GetFileName(PathUtils.TrimEndingDirectorySeparator(ArchiveEntry.FullName));
+ }
+ return ArchiveEntry.Name;
+ }
+ }
+
+ internal ZipArchive Archive {
+ get {
+ if (ArchiveEntry.Archive.Entries.Count == 0)
+ {
+ return null;
+ }
+ return ArchiveEntry.Archive;
+ }
+ }
+
+ internal ZipArchiveEntry ArchiveEntry {
+ get;
+ private set;
+
+ }
+
+ public FileInfo FileSystemContainer {
+ get {
+ return new FileInfo(Drive.Root);
+ }
+ }
+
+ public bool IsContainer {
+ get {
+ return PathUtils.EndsInDirectorySeparator(ArchiveEntry.FullName);
+ }
+ }
+
+ public ArchiveItemInfo(ZipArchiveEntry item, ArchivePSDriveInfo drive)
+ {
+ Drive = drive;
+ ArchiveEntry = item;
+ }
+
+ public ArchiveItemInfo(ArchivePSDriveInfo drive, string path) : this(drive, path, false)
+ {
+
+ }
+
+ public ArchiveItemInfo(ArchivePSDriveInfo drive, string path, bool createEntry)
+ {
+
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentNullException("path");
+ }
+
+ Drive = drive;
+
+ if (path.StartsWith(Drive.Name))
+ {
+ path = Path.GetRelativePath(Drive.Name + ":\\", path);
+ }
+ // Path.VolumeSeparatorChar defaults to a / in ubuntu
+ if (path.Contains( ":" ))
+ {
+ throw TraceSource.NewArgumentException(path);
+ }
+
+ path = path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ try {
+ ZipArchive zipArchive = drive.LockArchive(ArchiveProviderStrings.GetChildItemsAction);
+ ArchiveEntry = zipArchive.GetEntry(path);
+
+ if (ArchiveEntry == null)
+ {
+ if (createEntry == true)
+ {
+ // Create an entry if not exists
+ ArchiveEntry = zipArchive.CreateEntry(path);
+ //ArchiveEntry = zipArchive.GetEntry(path);
+
+ if (ArchiveEntry == null)
+ {
+ throw new IOException(ArchiveProviderStrings.PermissionError);
+ }
+ }
+ else
+ {
+ string error = String.Format(ArchiveProviderStrings.ItemNotFound, path);
+ throw new FileNotFoundException(error);
+ }
+
+ }
+
+ }
+ catch(Exception e) {
+ throw e;
+ }
+ finally {
+ drive.UnlockArchive(ArchiveProviderStrings.GetChildItemsAction);
+ }
+
+ }
+
+ public StreamWriter AppendText()
+ {
+ return new StreamWriter( OpenWrite() );
+ }
+
+ public void CopyTo(string destFileName)
+ {
+ CopyTo(destFileName, false, false);
+ }
+
+ public void CopyTo(string destFileName, bool overwrite)
+ {
+ CopyTo(destFileName, false, overwrite);
+ }
+ //Create Method System.IO.FileStream Create()
+ //CreateObjRef Method System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
+
+ public StreamWriter CreateText()
+ {
+ return new StreamWriter( OpenWrite() );
+ }
+
+ public void Delete()
+ {
+ try {
+ ZipArchive zipArchive = Drive.LockArchive(ArchiveEntry.FullName);
+ ZipArchiveEntry zipArchiveEntry = zipArchive.GetEntry(ArchiveEntry.FullName);
+ zipArchiveEntry.Delete();
+ }
+ catch {
+
+ }
+ finally {
+ Drive.UnlockArchive(ArchiveEntry.FullName);
+ }
+
+ }
+
+ public void Decrypt()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Encrypt()
+ {
+ throw new NotImplementedException();
+ }
+
+
+ //GetAccessControl Method System.Security.AccessControl.FileSecurity GetAccessControl(), System.Secur...
+ //GetHashCode Method int GetHashCode()
+ //GetLifetimeService Method System.Object GetLifetimeService()
+ //GetObjectData Method void GetObjectData(System.Runtime.Serialization.SerializationInfo info, Sys...
+ //GetType Method type GetType()
+ //InitializeLifetimeService Method System.Object InitializeLifetimeService()
+
+
+ public void MoveTo(string destFileName)
+ {
+ CopyTo(destFileName, true, false);
+ }
+
+ internal void CopyTo(string destFileName, bool removeItem, bool overwrite)
+ {
+ // if (destFileName.Contains(Path.GetInvalidPathChars()) || destFileName.Contains(Path.GetInvalidFileNameChars())
+ if (destFileName.IndexOfAny(Path.GetInvalidPathChars()) != -1)
+ {
+ throw new InvalidDataException("Path contains invalid characters");
+ }
+
+ // Convert Path to its proper dest path
+ destFileName = destFileName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ // If the destination file is a folder
+ // We should move/copy the item to that folder.
+ // Example:
+ // Move-Item Provider:\a\b\c\file.txt .\d\e\f
+ // Will move the file to Provider:\d\e\f\file.txt
+ if (destFileName.EndsWith(Path.AltDirectorySeparatorChar))
+ {
+ destFileName = $"{destFileName}{ArchiveEntry.Name}";
+ }
+
+ // Validate if path is filesystem
+ if (Path.IsPathRooted(destFileName) && !destFileName.StartsWith(Drive.Name))
+ {
+ CopyToFileSystem(destFileName, removeItem, overwrite);
+ return;
+ }
+
+ // Cleanup the filesystem path
+ if (destFileName.StartsWith(Drive.Name))
+ {
+ destFileName = Path.GetRelativePath((Drive.Name + ":\\"), destFileName);
+ }
+ else if (destFileName.StartsWith(Drive.Root))
+ {
+ destFileName = Path.GetRelativePath(Drive.Root, destFileName);
+ }
+
+ CopyToArchive(destFileName, removeItem, overwrite);
+ }
+
+ internal void CopyToFileSystem(string destFileName, bool removeItem, bool overwrite)
+ {
+ if (File.Exists(destFileName) && !overwrite)
+ {
+ throw new Exception($"The item exists '{destFileName}'");
+ }
+
+ ZipArchive zipArchive = Drive.LockArchive(FullArchiveName);
+ ZipArchiveEntry thisEntry = zipArchive.GetEntry(ArchiveEntry.FullName);
+
+ thisEntry.ExtractToFile(destFileName);
+
+ if (removeItem)
+ {
+ thisEntry.Delete();
+ }
+
+ Drive.UnlockArchive(FullArchiveName);
+ }
+
+ internal void CopyToArchive(string destFileName, bool removeItem, bool overwrite)
+ {
+ ZipArchive zipArchive = Drive.LockArchive(FullArchiveName);
+
+ ZipArchiveEntry thisEntry = zipArchive.GetEntry(ArchiveEntry.FullName);
+ ZipArchiveEntry newEntry = zipArchive.GetEntry(destFileName);
+
+ // Determine if Overwrite is enabled and item exists.
+ if ((overwrite == false) && (newEntry != null))
+ {
+ throw new Exception($"The item exists '{destFileName}'");
+ }
+
+ if (newEntry == null) {
+ newEntry = zipArchive.CreateEntry(destFileName);
+ }
+
+ using (Stream thisStream = thisEntry.Open())
+ using (Stream newStream = newEntry.Open())
+ {
+ thisStream.CopyTo(newStream);
+ }
+ if (removeItem)
+ {
+ thisEntry.Delete();
+ }
+
+ Drive.UnlockArchive(FullArchiveName);
+
+ }
+
+ public ArchiveItemStream Open()
+ {
+ return new ArchiveItemStream(this);
+ }
+
+ public ArchiveItemStream Open(FileMode mode)
+ {
+ return new ArchiveItemStream(this);
+ }
+
+ public ArchiveItemStream Open(FileMode mode, FileAccess access)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ArchiveItemStream Open(FileMode mode, FileAccess access, FileShare share)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ArchiveItemStream OpenRead()
+ {
+ return Open();
+ }
+
+ public StreamReader OpenText()
+ {
+ return new StreamReader(Open());
+ }
+
+ public ArchiveItemStream OpenWrite()
+ {
+ return Open();
+ }
+
+ //Refresh Method void Refresh()
+ //Replace Method System.IO.FileInfo Replace(string destinationFileName, string destinationBa...
+ //SetAccessControl Method void SetAccessControl(System.Security.AccessControl.FileSecurity fileSecurity)
+
+ public string ReadToEnd()
+ {
+ string result;
+ using (ArchiveItemStream stream = Open(FileMode.Append))
+ using (StreamReader streamReader = new StreamReader(stream))
+ {
+ result = streamReader.ReadToEnd();
+ }
+ return result;
+ }
+
+ internal void ClearContent()
+ {
+ ArchiveItemStream fileStream = Open(FileMode.Append);
+ fileStream.Seek(0, SeekOrigin.Begin);
+ fileStream.SetLength(0);
+ fileStream.Flush();
+ fileStream.Close();
+ fileStream.Dispose();
+ }
+
+ }
+ #endregion ArchiveItemInfo
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/ArchiveItemStream.cs b/src/Microsoft.PowerShell.Archive/ArchiveItemStream.cs
new file mode 100644
index 0000000..d9c0769
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/ArchiveItemStream.cs
@@ -0,0 +1,117 @@
+using System;
+using System.IO;
+using System.Management.Automation;
+using System.Management.Automation.Provider;
+using System.IO.Compression;
+
+namespace Microsoft.PowerShell.Archive
+{
+ #region ArchiveItemStream
+ public class ArchiveItemStream : System.IO.Stream
+ {
+
+ private ArchiveItemInfo _itemInfo;
+ public System.IO.Stream _stream;
+
+ public bool _isClosed;
+ public override long Length {
+ get
+ {
+ return _stream.Length;
+ }
+ }
+ public override long Position {
+ get
+ {
+ return _stream.Position;
+ }
+ set
+ {
+ _stream.Position = value;
+ }
+ }
+
+ public override bool CanSeek
+ {
+ get
+ {
+ return _stream.CanSeek;
+ }
+ }
+ public override bool CanRead {
+ get
+ {
+ return _stream.CanRead;
+ }
+ }
+
+ public override bool CanWrite {
+ get
+ {
+ return _stream.CanWrite;
+ }
+ }
+
+ public override void Flush()
+ {
+ _stream.Flush();
+ }
+ public override void SetLength(long value)
+ {
+ _stream.SetLength(value);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _stream.Read(buffer, offset, count);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _stream.Write(buffer, offset, count);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ return _stream.Seek(offset, origin);
+ }
+
+ public ArchiveItemStream(ArchiveItemInfo entry)
+ {
+ _itemInfo = entry;
+
+ ZipArchive archive = _itemInfo.Drive.LockArchive(_itemInfo.ArchiveEntry.FullName);
+
+ _stream = archive.GetEntry(_itemInfo.ArchiveEntry.FullName).Open();
+ // Sets position to 0 so it can be fresh
+ _stream.Position = 0;
+ }
+ public override void Close()
+ {
+ if (!_isClosed)
+ {
+ _stream.Flush();
+ _stream.Dispose();
+
+ _itemInfo.Drive.UnlockArchive(_itemInfo.ArchiveEntry.FullName);
+
+ _isClosed = true;
+ base.Close();
+
+ GC.Collect();
+ }
+
+ }
+
+ public void Dispose()
+ {
+ base.Dispose();
+ }
+
+ ~ArchiveItemStream()
+ {
+ Dispose();
+ }
+ }
+ #endregion ArchiveItemStream
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/ArchivePSDriveInfo.cs b/src/Microsoft.PowerShell.Archive/ArchivePSDriveInfo.cs
new file mode 100644
index 0000000..6d76089
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/ArchivePSDriveInfo.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation;
+using System.Management.Automation.Provider;
+using System.IO;
+using System.IO.Compression;
+
+namespace Microsoft.PowerShell.Archive
+{
+
+ public static class PathUtils
+ {
+ public static bool EndsInDirectorySeparator(string path)
+ {
+ if (path.EndsWith(Path.AltDirectorySeparatorChar))
+ return true;
+ if (path.EndsWith(Path.DirectorySeparatorChar))
+ return true;
+ return false;
+ }
+ public static string TrimEndingDirectorySeparator(string path)
+ {
+ path = path.TrimEnd(Path.DirectorySeparatorChar).TrimEnd(Path.AltDirectorySeparatorChar);
+ return path;
+ }
+
+
+ }
+
+ public class ArchivePSDriveInfo : PSDriveInfo
+ {
+ internal ZipArchive Archive {
+ get;
+ private set;
+ }
+ private Dictionary _streamsInUse;
+
+ private FileSystemWatcher _fileWatcher;
+ private int _fileWatcherLock = 0;
+
+ private List _entryCache;
+
+ //internal bool IsStreamInUse()
+ //internal void OpenStream()
+ //internal void CloseStream()
+
+ //internal Stream PullStream() // Note this should not be used
+
+ public List _lockedEntries = new List();
+ public ZipArchive LockArchive(string entry)
+ {
+ if (_lockedEntries.Contains(entry))
+ {
+ throw new Exception("Cannot open file it is already open in another process");
+ }
+ _lockedEntries.Add(entry);
+
+ if (Archive == null)
+ {
+ Archive = ZipFile.Open(Root, ZipArchiveMode.Update);
+ }
+
+ return Archive;
+ }
+
+ public void UnlockArchive(string entry)
+ {
+ UnlockArchive(entry, false);
+ }
+ public void UnlockArchive(string entry, bool updateCache)
+ {
+ if (!_lockedEntries.Contains(entry))
+ {
+ throw new Exception("Cannot unlock stream it doesnt exist");
+ }
+
+ if (!updateCache)
+ {
+ _entryCache = null;
+ }
+
+ _lockedEntries.Remove(entry);
+
+ if (_lockedEntries.Count == 0)
+ {
+ Archive.Dispose();
+
+ Archive = null;
+ GC.Collect();
+ }
+ }
+
+ internal bool IsStreamInUse()
+ {
+ if (Archive != null)
+ {
+ return true;
+ }
+ return false;
+ }
+ public int ActiveHandles {
+ get {
+ return _lockedEntries.Count;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the AccessDBPSDriveInfo class.
+ /// The constructor takes a single argument.
+ ///
+ /// Drive defined by this provider
+ public ArchivePSDriveInfo(PSDriveInfo driveInfo) : base(driveInfo)
+ {
+ UpdateCache();
+ }
+
+
+ #region ItemCache
+
+ ///
+ /// Updates the cached entries.
+ ///
+ protected private void UpdateCache()
+ {
+ try
+ {
+ _entryCache = new List();
+ ZipArchive zipArchive = LockArchive(ArchiveProviderStrings.GetChildItemsAction);
+
+ foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries)
+ {
+ _entryCache.Add( new ArchiveItemInfo(zipArchiveEntry, this) );
+ }
+ }
+ catch(Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ UnlockArchive(ArchiveProviderStrings.GetChildItemsAction, true);
+ }
+ }
+
+ #endregion ItemCache
+
+ #region ItemHandler
+
+ public IEnumerable GetItem()
+ {
+ if (_entryCache == null)
+ {
+ UpdateCache();
+ }
+ //UpdateCache();
+ foreach (ArchiveItemInfo item in _entryCache)
+ {
+ yield return item;
+ }
+ }
+
+ public IEnumerable GetItem(string path)
+ {
+ IEnumerable results = GetItem();
+
+ path = PathUtils.TrimEndingDirectorySeparator(path).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+
+ WildcardPattern wildcardPattern = WildcardPattern.Get(path, WildcardOptions.IgnoreCase | WildcardOptions.Compiled);
+
+ foreach (ArchiveItemInfo item in results)
+ {
+ if (wildcardPattern.IsMatch(PathUtils.TrimEndingDirectorySeparator( item.FullArchiveName )))
+ {
+ yield return item;
+ }
+ }
+ }
+
+ public IEnumerable GetItem(string path, bool directory, bool file)
+ {
+ IEnumerable results = GetItem(path);
+ WildcardPattern wildcardPattern = WildcardPattern.Get(path, WildcardOptions.IgnoreCase | WildcardOptions.Compiled);
+ path = path.TrimStart(Path.AltDirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar);
+
+ foreach (ArchiveItemInfo item in results)
+ {
+ if ( Path.GetDirectoryName(path) != Path.GetDirectoryName( PathUtils.TrimEndingDirectorySeparator(item.FullArchiveName) ) )
+ {
+ continue;
+ }
+
+ if ((directory && item.IsContainer) || (file && !item.IsContainer))
+ {
+ yield return item;
+ }
+
+ }
+ }
+
+ public bool ItemExists(string path)
+ {
+ // Return true if either condition is met.
+ return ItemExists(path, false) || ItemExists(path, true);
+ }
+
+ public bool ItemExists(string path, bool directory)
+ {
+ path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+
+ List items = GetItem().ToList();
+
+ foreach (ArchiveItemInfo i in items)
+ {
+ if (!directory && (path == i.FullArchiveName))
+ {
+ return true;
+ }
+
+ if (directory && PathUtils.EndsInDirectorySeparator(i.FullArchiveName) && (PathUtils.TrimEndingDirectorySeparator(path) == PathUtils.TrimEndingDirectorySeparator(i.FullArchiveName)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public bool IsItemContainer(string path)
+ {
+ return ItemExists(path, true);
+ }
+
+ #endregion ItemHandler
+ public void buildFolderPaths()
+ {
+
+ try {
+ ZipArchive zipArchive = LockArchive(ArchiveProviderStrings.GetChildItemsAction);
+
+ // Generate a list of items to create
+ List dirList = new List();
+ foreach (ZipArchiveEntry entry in zipArchive.Entries)
+ {
+ string fullName = entry.FullName;
+ if (PathUtils.EndsInDirectorySeparator(fullName))
+ {
+ continue;
+ }
+
+ fullName = Path.GetDirectoryName(fullName) + Path.AltDirectorySeparatorChar;
+ fullName = fullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ if (String.IsNullOrEmpty(fullName))
+ {
+ continue;
+ }
+ var paths = enumFolderPaths(fullName);
+
+ foreach (string path in paths)
+ {
+ if (zipArchive.GetEntry(path) == null)
+ {
+ if (!dirList.Contains(path))
+ {
+ dirList.Add(path);
+ }
+ }
+ }
+ }
+
+ // Generate a list of directories
+ foreach (string dir in dirList)
+ {
+ zipArchive.CreateEntry(dir);
+ }
+
+ }
+ catch(Exception e) {
+ throw e;
+ }
+ finally {
+ UnlockArchive(ArchiveProviderStrings.GetChildItemsAction);
+ }
+ }
+
+ private static IEnumerable enumFolderPaths(string path)
+ {
+ int i = 0;
+ while((i = path.IndexOf(Path.AltDirectorySeparatorChar, i+1)) > -1)
+ {
+ yield return path.Substring(0, i+1);
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/ArchiveProvider.cs b/src/Microsoft.PowerShell.Archive/ArchiveProvider.cs
new file mode 100644
index 0000000..bada67f
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/ArchiveProvider.cs
@@ -0,0 +1,1753 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Management.Automation;
+using System.Management.Automation.Provider;
+using System.Linq;
+using System.Text;
+
+using Microsoft.PowerShell.Commands;
+
+namespace Microsoft.PowerShell.Archive
+{
+ #region ArchiveProvider : IContentReader, IContentWriter
+ [CmdletProvider(ArchiveProvider.ProviderName, ProviderCapabilities.ShouldProcess | ProviderCapabilities.ExpandWildcards )]
+ public class ArchiveProvider : NavigationCmdletProvider, IContentCmdletProvider
+ {
+
+ ///
+ /// Gets the name of the provider.
+ ///
+ public const string ProviderName = "Microsoft.PowerShell.Archive";
+
+ // Workaround for internal class objects
+ internal InvocationInfo Context_MyInvocation {
+ get {
+ return (InvocationInfo)SessionState.PSVariable.Get("MyInvocation").Value;
+ }
+ }
+
+ internal ArchivePSDriveInfo ArchiveDriveInfo {
+ get {
+ if (_psDriveInfo != null)
+ {
+ return _psDriveInfo;
+ }
+ return (PSDriveInfo as ArchivePSDriveInfo);
+ }
+ private set
+ {
+ _psDriveInfo = value;
+ }
+ }
+
+ internal ArchivePSDriveInfo _psDriveInfo;
+
+ ///
+ /// Initializes a new instance of the FileSystemProvider class. Since this
+ /// object needs to be stateless, the constructor does nothing.
+ ///
+ public ArchiveProvider()
+ {
+
+ }
+
+ ///
+ /// Converts all / in the path to \
+ ///
+ ///
+ ///
+ /// The path to normalize.
+ ///
+ ///
+ ///
+ /// The path with all / normalized to \
+ /// and resolve the path based off of its Root/Name
+ ///
+ private string NormalizePath(string path)
+ {
+
+ // [Bug] PSDriveInfo sometimes does not get instantiated with the provider
+ // this causes stateful issues with complex providers.
+ // Example Duplication of this issue
+ //
+ // ./
+ // and
+ // Get-Item $FileName | Remove-Item
+ //
+ // Current Workaround searches all Drives with ProviderName
+ // and checks relative path and overrides the path lookup.
+
+ if (PSDriveInfo == null) {
+ if (path.Contains(Path.VolumeSeparatorChar))
+ {
+ SessionState.Drive.GetAllForProvider(ProviderName).ToList().ForEach( i => {
+ if ( (path.StartsWith(i.Root)) || (path.StartsWith(i.Name)) )
+ {
+ ArchiveDriveInfo = (i as ArchivePSDriveInfo);
+ }
+ });
+ }
+ }
+
+ // Null or empty should return null or empty
+ if (String.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1)
+ {
+ TraceSource.NewArgumentException(ArchiveProviderStrings.PathContainsInvalidCharacters);
+ }
+
+ if (path.StartsWith($"{ArchiveDriveInfo.Root}"))
+ {
+ path = path.Remove(0, ArchiveDriveInfo.Root.Length);
+ }
+ else if (path.StartsWith($"{ArchiveDriveInfo.Name}:") )
+ {
+ path = path.Remove(0, ArchiveDriveInfo.Name.Length+1);
+ }
+
+ path = path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ path = path.TrimStart(Path.AltDirectorySeparatorChar);
+
+ // Before returning a normalized path
+ return path;
+ }
+
+ #region ICmdletProviderSupportsHelp members
+
+ #endregion
+ #region CmdletProvider members
+
+ #endregion CmdletProvider members
+ #region DriveCmdletProvider members
+
+ ///
+ /// Determines if the specified drive can be mounted.
+ ///
+ ///
+ ///
+ /// The drive that is going to be mounted.
+ ///
+ ///
+ ///
+ /// The same drive that was passed in, if the drive can be mounted.
+ /// null if the drive cannot be mounted.
+ ///
+ ///
+ /// drive is null.
+ ///
+ ///
+ /// drive root is null or empty.
+ ///
+ protected override PSDriveInfo NewDrive(PSDriveInfo drive)
+ {
+ // verify parameters
+
+ if (drive == null)
+ {
+ throw TraceSource.NewArgumentNullException("drive");
+ }
+
+ if (String.IsNullOrEmpty(drive.Root))
+ {
+ throw TraceSource.NewArgumentException("drive.Root");
+ }
+
+ FileInfo archiveInfo = new FileInfo(
+ Path.GetFullPath(drive.Root, SessionState.Path.CurrentLocation.Path)
+ );
+
+ if (!File.Exists(archiveInfo.FullName))
+ {
+ throw new Exception("file not found");
+ }
+
+ drive = new PSDriveInfo(drive.Name, drive.Provider, archiveInfo.FullName, drive.Description, drive.Credential, drive.DisplayRoot);
+ ArchivePSDriveInfo newDrive = new ArchivePSDriveInfo(drive);
+
+ // Build folder paths on initialize
+ newDrive.buildFolderPaths();
+
+ return base.NewDrive( newDrive );
+ }
+
+ #endregion DriveCmdletProvider methods
+ #region ItemCmdletProvider methods
+
+ ///
+ /// Determines if the specified path is syntactically and semantically valid.
+ /// An example path looks like this
+ /// C:\WINNT\Media\chimes.wav.
+ ///
+ ///
+ /// The fully qualified path to validate.
+ ///
+ ///
+ /// True if the path is valid, false otherwise.
+ ///
+ protected override bool IsValidPath(string path)
+ {
+ // Path passed should be fully qualified path.
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ // Normalize the path
+ path = NormalizePath(path);
+ // path = EnsureDriveIsRooted(path);
+
+ // Make sure the path is either drive rooted or UNC Path
+ if (!IsAbsolutePath(path) && !PathIsUnc(path))
+ {
+ return false;
+ }
+
+ // Exceptions should only deal with exceptional circumstances,
+ // but unfortunately, FileInfo offers no Try() methods that
+ // let us check if we _could_ open the file.
+ try
+ {
+ ArchiveItemInfo testFile = new ArchiveItemInfo(ArchiveDriveInfo, path);
+ }
+ catch (Exception e)
+ {
+ if ((e is ArgumentNullException) ||
+ (e is ArgumentException) ||
+ (e is System.Security.SecurityException) ||
+ (e is UnauthorizedAccessException) ||
+ (e is PathTooLongException) ||
+ (e is NotSupportedException))
+ {
+ return false;
+ }
+ else
+ {
+ throw;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Expand a provider path that contains wildcards to a list of provider paths that the
+ /// path represents. Only called for providers that declare the ExpandWildcards capability.
+ ///
+ ///
+ ///
+ /// The path to expand. Expansion must be consistent with the wildcarding rules of PowerShell's WildcardPattern class.
+ ///
+ ///
+ ///
+ /// A list of provider paths that this path expands to. They must all exist.
+ ///
+ ///
+ protected override string[] ExpandPath(string path)
+ {
+ path = NormalizePath(path);
+ IEnumerable ArchiveItemInfoList = ArchiveDriveInfo.GetItem(path, true, true);
+ return ArchiveItemInfoList.Select(i => i.FullName).ToArray();
+ }
+
+ ///
+ /// Gets the item at the specified path.
+ ///
+ ///
+ /// A fully qualified path representing a file or directory in the
+ /// file system.
+ ///
+ ///
+ /// Nothing. FileInfo and DirectoryInfo objects are written to the
+ /// context's pipeline.
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override void GetItem(string path)
+ {
+
+ path = NormalizePath(path);
+
+ // Validate the argument
+ bool isContainer = false;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ // The parameter was null, throw an exception
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ try
+ {
+
+ IEnumerable result = ArchiveDriveInfo.GetItem(path, true, true);
+
+ if (result != null)
+ {
+ // Otherwise, return the item itself.
+ foreach (ArchiveItemInfo i in result) {
+ WriteItemObject(i, i.FullName, isContainer);
+ }
+ //
+ //WriteItemObject(result, path, )
+ }
+ else
+ {
+ string error = String.Format(ArchiveProviderStrings.ItemNotFound, path);
+ Exception e = new IOException(error);
+ WriteError(new ErrorRecord(
+ e,
+ "ItemNotFound",
+ ErrorCategory.ObjectNotFound,
+ path));
+ }
+ }
+ catch (IOException ioError)
+ {
+ // IOException contains specific message about the error occured and so no need for errordetails.
+ ErrorRecord er = new ErrorRecord(ioError, "GetItemIOError", ErrorCategory.ReadError, path);
+ WriteError(er);
+ }
+ catch (UnauthorizedAccessException accessException)
+ {
+ WriteError(new ErrorRecord(accessException, "GetItemUnauthorizedAccessError", ErrorCategory.PermissionDenied, path));
+ }
+ }
+
+ ///
+ /// Invokes the item at the path using ShellExecute semantics.
+ ///
+ ///
+ ///
+ /// The item to invoke.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override void InvokeDefaultAction(string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ string action = ArchiveProviderStrings.InvokeItemAction;
+
+ string resource = String.Format(ArchiveProviderStrings.InvokeItemResourceFileTemplate, path);
+
+ if (ShouldProcess(resource, action))
+ {
+ var invokeProcess = new System.Diagnostics.Process();
+ invokeProcess.StartInfo.FileName = path;
+
+ bool invokeDefaultProgram = false;
+
+
+ if (IsItemContainer(path))
+ {
+
+ // Path points to a directory. We have to use xdg-open/open on Linux/macOS.
+ invokeDefaultProgram = true;
+ path = ArchiveDriveInfo.Root;
+ }
+ else if (Path.GetExtension(path) == ".ps1") {
+
+ IEnumerable archiveItemInfoList = ArchiveDriveInfo.GetItem(path, false, true);
+ Object[] scriptargs = null;
+ foreach (ArchiveItemInfo archiveItemInfo in archiveItemInfoList)
+ {
+ string script = archiveItemInfo.ReadToEnd();
+ ScriptBlock scriptBlock = ScriptBlock.Create(script);
+ var result = SessionState.InvokeCommand.InvokeScript(SessionState, scriptBlock, scriptargs);
+ WriteItemObject(result, archiveItemInfo.FullName, false);
+ }
+ }
+
+ if (invokeDefaultProgram)
+ {
+ const string quoteFormat = "\"{0}\"";
+
+ if (Platform.IsLinux) {
+ invokeProcess.StartInfo.FileName = "xdg-open";
+ invokeProcess.StartInfo.Arguments = path;
+ }
+ if (Platform.IsMacOS) {
+ invokeProcess.StartInfo.FileName = "open";
+ invokeProcess.StartInfo.Arguments = path;
+ }
+ if (Platform.IsWindows)
+ {
+ // Use ShellExecute when it's not a headless SKU
+ //
+ invokeProcess.StartInfo.UseShellExecute = Platform.IsWindowsDesktop;
+ invokeProcess.StartInfo.FileName = path;
+ }
+ //if (NativeCommandParameterBinder.NeedQuotes(path))
+ {
+ // Assume true
+ path = string.Format(CultureInfo.InvariantCulture, quoteFormat, path);
+ }
+ invokeProcess.Start();
+ }
+ }
+ } // InvokeDefaultAction
+ #endregion ItemCmdletProvider members
+ #region ContainerCmdletProvider members
+ #region GetChildItems
+ ///
+ /// Gets the child items of a given directory.
+ ///
+ ///
+ ///
+ /// The full path of the directory to enumerate.
+ ///
+ ///
+ ///
+ /// If true, recursively enumerates the child items as well.
+ ///
+ ///
+ ///
+ /// Limits the depth of recursion; uint.MaxValue performs full recursion.
+ ///
+ ///
+ ///
+ /// Nothing. FileInfo and DirectoryInfo objects that match the filter are written to the
+ /// context's pipeline.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override void GetChildItems(
+ string path,
+ bool recurse,
+ uint depth)
+ {
+ GetPathItems(path, recurse, depth, false, ReturnContainers.ReturnMatchingContainers);
+ } // GetChildItems
+ #endregion GetChildItems
+ #region GetChildNames
+ ///
+ /// Gets the path names for all children of the specified
+ /// directory that match the given filter.
+ ///
+ ///
+ ///
+ /// The full path of the directory to enumerate.
+ ///
+ ///
+ ///
+ /// Determines if all containers should be returned or only those containers that match the
+ /// filter(s).
+ ///
+ ///
+ ///
+ /// Nothing. Child names are written to the context's pipeline.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override void GetChildNames(
+ string path,
+ ReturnContainers returnContainers)
+ {
+ GetPathItems(path, false, uint.MaxValue, true, returnContainers);
+ } // GetChildNames
+
+ #endregion GetChildNames
+ protected override bool ConvertPath(
+ string path,
+ string filter,
+ ref string updatedPath,
+ ref string updatedFilter)
+ {
+
+ // Don't handle full paths, paths that the user is already trying to
+ // filter, or paths they are trying to escape.
+ if ((!string.IsNullOrEmpty(filter)) ||
+ (path.Contains(Path.DirectorySeparatorChar, StringComparison.Ordinal)) ||
+ (path.Contains(Path.AltDirectorySeparatorChar, StringComparison.Ordinal)) ||
+ (path.Contains('`'))
+ )
+ {
+ return false;
+ }
+ WriteWarning("I should resolve a path ${path}");
+ // We can never actually modify the PowerShell path, as the
+ // Win32 filtering support returns items that match the short
+ // filename OR long filename.
+ //
+ // This creates tons of seemingly incorrect matches, such as:
+ //
+ // *~*: Matches any file with a long filename
+ // *n*: Matches all files with a long filename, but have been
+ // mapped to a [6][~n].[3] disambiguation bucket
+ // *.abc: Matches all files that have an extension that begins
+ // with ABC, since their extension is truncated in the
+ // short filename
+ // *.*: Matches all files and directories, even if they don't
+ // have a dot in their name
+
+ // Our algorithm here is pretty simple. The filesystem can handle
+ // * and ? in PowerShell wildcards, just not character ranges [a-z].
+ // We replace character ranges with the single-character wildcard, '?'.
+ updatedPath = path;
+ updatedFilter = System.Text.RegularExpressions.Regex.Replace(path, "\\[.*?\\]", "?");
+
+ return true;
+ }
+
+ private void GetPathItems(
+ string path,
+ bool recurse,
+ uint depth,
+ bool nameOnly,
+ ReturnContainers returnContainers)
+ {
+
+ // Verify parameters
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ bool isDirectory = IsItemContainer(path);
+ bool exists = ItemExists(path);
+
+ path = NormalizePath(path);
+
+ if (IsItemContainer(path))
+ {
+ path += Path.AltDirectorySeparatorChar;
+ }
+
+ if (exists)
+ {
+ //path = String.IsNullOrEmpty(path) || !path.StartsWith(ArchiveDriveInfo.Name) ? $"{ArchiveDriveInfo.Name}:\\{path}" : path;
+
+ if (isDirectory)
+ {
+ if (!path.Contains("*"))
+ {
+ path += "*";
+ }
+
+ path = path.TrimStart(Path.AltDirectorySeparatorChar);
+
+ // Only the Root directory is looked at for this scenario.
+ List fileInfoItems = ArchiveDriveInfo.GetItem(path, true, true).ToList();
+
+ if (fileInfoItems.Count == 0)
+ {
+ return;
+ }
+
+ // Sort the files
+ fileInfoItems = fileInfoItems.OrderBy(c => c.FullName, StringComparer.CurrentCultureIgnoreCase).ToList();
+
+
+ foreach (ArchiveItemInfo fileInfo in fileInfoItems)
+ {
+ if (nameOnly)
+ {
+ WriteItemObject(
+ fileInfo.Name,
+ fileInfo.FullName,
+ fileInfo.IsContainer);
+ }
+ else
+ {
+ WriteItemObject(fileInfo, fileInfo.FullName, fileInfo.IsContainer);
+ }
+ }
+
+ }
+ else
+ {
+ // Maybe the path is a file name so try a FileInfo instead
+ ArchiveItemInfo fileInfo = new ArchiveItemInfo(ArchiveDriveInfo, path);
+
+ if (nameOnly)
+ {
+ WriteItemObject(
+ fileInfo.Name,
+ fileInfo.FullName,
+ false);
+ }
+ else
+ {
+ WriteItemObject(fileInfo, fileInfo.FullName, false);
+ }
+
+ }
+
+ }
+ else
+ {
+ Console.WriteLine("Please help me out. Submit an issue with what you did in order to get this to trigger");
+ Console.WriteLine("https://github.com/romero126/PS1C");
+
+ String error = String.Format(ArchiveProviderStrings.ItemDoesNotExist, path);
+ Exception e = new IOException(error);
+ WriteError(new ErrorRecord(
+ e,
+ "ItemDoesNotExist",
+ ErrorCategory.ObjectNotFound,
+ path));
+ return;
+ }
+ }
+
+ #region RenameItem
+ ///
+ /// Renames a file or directory.
+ ///
+ ///
+ ///
+ /// The current full path to the file or directory.
+ ///
+ ///
+ ///
+ /// The new full path to the file or directory.
+ ///
+ ///
+ ///
+ /// Nothing. The renamed DirectoryInfo or FileInfo object is
+ /// written to the context's pipeline.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ /// newName is null or empty
+ ///
+ protected override void RenameItem(string path, string newName)
+ {
+
+ // Check the parameters
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ if (String.IsNullOrEmpty(newName))
+ {
+ throw TraceSource.NewArgumentException("newName");
+ }
+
+ // newName = NormalizePath(newName);
+
+ // Clean up "newname" to fix some common usability problems:
+ // Rename .\foo.txt .\bar.txt
+ // Rename c:\temp\foo.txt c:\temp\bar.txt
+ if (newName.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) ||
+ newName.StartsWith("./", StringComparison.OrdinalIgnoreCase))
+ {
+ newName = newName.Remove(0, 2);
+ }
+ // else if (String.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(newName), StringComparison.OrdinalIgnoreCase))
+ // {
+ // newName = Path.GetFileName(newName);
+ // }
+
+ //Check to see if the target specified is just filename. We dont allow rename to move the file to a different directory.
+ //If a path is specified for the newName then we flag that as an error.
+ // if (String.Compare(Path.GetFileName(newName), newName, StringComparison.OrdinalIgnoreCase) != 0)
+ // {
+ // throw TraceSource.NewArgumentException("newName", ArchiveProviderStrings.RenameError);
+ // }
+
+ // Check to see if the target specified exists.
+ if (ItemExists(newName))
+ {
+ throw TraceSource.NewArgumentException("newName", ArchiveProviderStrings.RenameError);
+ }
+
+ try
+ {
+ // Manually move this item since you cant have more than one stream open at a time.
+ ArchiveItemInfo file = new ArchiveItemInfo(ArchiveDriveInfo, path);
+ ArchiveItemInfo result;
+
+ // Confirm the rename with the user
+
+ string action = ArchiveProviderStrings.RenameItemActionFile;
+
+ string resource = String.Format(ArchiveProviderStrings.RenameItemResourceFileTemplate, file.FullName, newName);
+
+
+ if (ShouldProcess(resource, action))
+ {
+ // Now move the file
+ // Validate Current PWD is not the Provider
+ //if ((!Path.IsPathFullyQualified(newName)) && (!SessionState.Path.CurrentLocation.Path.StartsWith(ArchiveDriveInfo.Name + ":")) )
+ //{
+ // newName = Path.Join(SessionState.Path.CurrentLocation.Path, newName);
+ //}
+
+ file.MoveTo(newName);
+
+ result = file;
+ WriteItemObject(result, result.FullName, false);
+ }
+ }
+ catch (ArgumentException argException)
+ {
+ WriteError(new ErrorRecord(argException, "RenameItemArgumentError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (IOException ioException)
+ {
+ //IOException contains specific message about the error occured and so no need for errordetails.
+ WriteError(new ErrorRecord(ioException, "RenameItemIOError", ErrorCategory.WriteError, path));
+ }
+ catch (UnauthorizedAccessException accessException)
+ {
+ WriteError(new ErrorRecord(accessException, "RenameItemUnauthorizedAccessError", ErrorCategory.PermissionDenied, path));
+ }
+ }
+ #endregion RenameItem
+ #region NewItem
+ ///
+ /// Creates a file or directory with the given path.
+ ///
+ ///
+ /// The path of the file or directory to create.
+ ///
+ ///
+ /// Specify "file" to create a file.
+ /// Specify "directory" or "container" to create a directory.
+ ///
+ ///
+ /// If is "file" then this parameter becomes the content
+ /// of the file to be created.
+ ///
+ ///
+ /// Nothing. The new DirectoryInfo or FileInfo object is
+ /// written to the context's pipeline.
+ ///
+ ///
+ /// path is null or empty.
+ /// type is null or empty.
+ ///
+ protected override void NewItem(
+ string path,
+ string type,
+ object value)
+ {
+ ItemType itemType = ItemType.Unknown;
+ bool CreateIntermediateDirectories = false;
+
+ // Verify parameters
+ if (string.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ if (String.IsNullOrEmpty(type))
+ {
+ type = "file";
+ }
+
+ itemType = GetItemType(type);
+
+ // Determine item Type
+ if (itemType == ItemType.Unknown)
+ {
+ if (PathUtils.EndsInDirectorySeparator(path))
+ {
+ itemType = ItemType.Directory;
+ }
+ else
+ {
+ itemType = ItemType.File;
+ }
+ }
+
+ path = NormalizePath(path);
+
+ try {
+
+ if (Force)
+ {
+ ArchiveItemInfo NewFile = new ArchiveItemInfo(ArchiveDriveInfo, path, true);
+ ArchiveDriveInfo.buildFolderPaths();
+ }
+
+ // Validate Parent Directory does not exist
+ if (!IsItemContainer(Path.GetDirectoryName(path)) && !Force)
+ {
+ WriteError(new ErrorRecord(
+ new IOException("Parent directory does not exist"),
+ "NewItemIOError",
+ ErrorCategory.WriteError,
+ path
+ ));
+ return;
+ }
+
+ if (IsItemContainer(path) && itemType == ItemType.File)
+ {
+ throw new UnauthorizedAccessException("No Access");
+ }
+
+ if (ItemExists(path) && !Force)
+ {
+ throw new Exception("File Exists");
+ }
+
+ if (itemType == ItemType.Directory)
+ {
+ string action = ArchiveProviderStrings.NewItemActionDirectory;
+
+ string resource = String.Format(ArchiveProviderStrings.NewItemActionTemplate, path);
+
+ if (!ShouldProcess(resource, action))
+ {
+ return;
+ }
+
+ if (!PathUtils.EndsInDirectorySeparator(path))
+ {
+ path += Path.AltDirectorySeparatorChar;
+ }
+
+ ArchiveItemInfo newItem = new ArchiveItemInfo(ArchiveDriveInfo, path, true);
+
+ }
+ else if (itemType == ItemType.File)
+ {
+ string action = ArchiveProviderStrings.NewItemActionFile;
+
+ string resource = String.Format(ArchiveProviderStrings.NewItemActionTemplate, path);
+
+ if (!ShouldProcess(resource, action))
+ {
+ return;
+ }
+
+ ArchiveItemInfo newItem = new ArchiveItemInfo(ArchiveDriveInfo, path, true);
+ newItem = new ArchiveItemInfo(ArchiveDriveInfo, path, true);
+ if (value != null)
+ {
+ using (StreamWriter writer = newItem.AppendText())
+ {
+ writer.Write(value.ToString());
+ writer.Flush();
+ writer.Dispose();
+ }
+ }
+ }
+ }
+ catch(Exception exception) {
+ //rollback the directory creation if it was created.
+ // if (!pathExists)
+ // {
+ // pathDirInfo.Delete();
+ // }
+
+ if ((exception is FileNotFoundException) ||
+ (exception is DirectoryNotFoundException) ||
+ (exception is UnauthorizedAccessException) ||
+ (exception is System.Security.SecurityException) ||
+ (exception is ArgumentException) ||
+ (exception is PathTooLongException) ||
+ (exception is NotSupportedException) ||
+ (exception is ArgumentNullException) ||
+ (exception is IOException))
+ {
+ WriteError(new ErrorRecord(exception, "NewItemCreateIOError", ErrorCategory.WriteError, path));
+ }
+ else
+ throw;
+ }
+ }
+
+ private enum ItemType
+ {
+ Unknown,
+ File,
+ Directory
+ };
+
+ private static ItemType GetItemType(string input)
+ {
+ ItemType itemType = ItemType.Unknown;
+
+ WildcardPattern typeEvaluator =
+ WildcardPattern.Get(input + "*",
+ WildcardOptions.IgnoreCase |
+ WildcardOptions.Compiled);
+
+ if (typeEvaluator.IsMatch("directory") ||
+ typeEvaluator.IsMatch("container"))
+ {
+ itemType = ItemType.Directory;
+ }
+ else if (typeEvaluator.IsMatch("file"))
+ {
+ itemType = ItemType.File;
+ }
+
+ return itemType;
+ }
+
+ #endregion NewItem
+ #region RemoveItem
+ ///
+ /// Removes the specified file or directory.
+ ///
+ ///
+ /// The full path to the file or directory to be removed.
+ ///
+ ///
+ /// Specifies if the operation should also remove child items.
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override void RemoveItem(string path, bool recurse)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ try {
+ path = NormalizePath(path);
+
+ if (!ItemExists(path))
+ {
+ WriteError(
+ new ErrorRecord(
+ new IOException(String.Format(ArchiveProviderStrings.ItemDoesNotExist, path)),
+ "ItemDoesNotExist",
+ ErrorCategory.ObjectNotFound,
+ path
+ )
+
+ );
+ return;
+ }
+
+ bool isItemContainer = IsItemContainer(path) && IsItemContainerContainsItems(path);
+
+ if (!recurse && isItemContainer)
+ {
+ throw new Exception("Folder contains subitems");
+ }
+
+ IEnumerable archiveItems;
+ if (isItemContainer)
+ {
+ // Recursivly remove items
+
+ archiveItems = ArchiveDriveInfo.GetItem(path+"*");
+ }
+ else {
+ archiveItems = ArchiveDriveInfo.GetItem(path, true, true);
+ }
+
+ // Item ToArray skips a file open bug.
+ foreach(ArchiveItemInfo archiveItem in archiveItems.ToArray())
+ {
+ string action = $"Do you want to remove current file?";
+ if (ShouldProcess(archiveItem.FullName, action))
+ {
+ archiveItem.Delete();
+ } // ShouldProcess
+ }
+
+ }
+ catch(Exception exception) {
+ if ((exception is FileNotFoundException) ||
+ (exception is DirectoryNotFoundException) ||
+ (exception is UnauthorizedAccessException) ||
+ (exception is System.Security.SecurityException) ||
+ (exception is ArgumentException) ||
+ (exception is PathTooLongException) ||
+ (exception is NotSupportedException) ||
+ (exception is ArgumentNullException) ||
+ (exception is IOException))
+ {
+ WriteError(new ErrorRecord(exception, "NewItemCreateIOError", ErrorCategory.WriteError, path));
+ }
+ else
+ Console.WriteLine("An Error was thrown");
+ throw;
+ }
+
+ }
+
+ #endregion RemoveItem
+ #region ItemExists
+ ///
+ /// Determines if a file or directory exists at the specified path.
+ ///
+ ///
+ ///
+ /// The path of the item to check.
+ ///
+ ///
+ ///
+ /// True if a file or directory exists at the specified path, false otherwise.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ ///
+
+ protected override bool ItemExists(string path)
+ {
+ ErrorRecord error = null;
+
+ bool result = ItemExists(path, out error);
+ if (error != null)
+ {
+ WriteError(error);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Implementation of ItemExists for the provider. This implementation
+ /// allows the caller to decide if it wants to WriteError or not based
+ /// on the returned ErrorRecord
+ ///
+ ///
+ ///
+ /// The path of the object to check
+ ///
+ ///
+ ///
+ /// An error record is returned in this parameter if there was an error.
+ ///
+ ///
+ ///
+ /// True if an object exists at the specified path, false otherwise.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ ///
+
+ private bool ItemExists(string path, out ErrorRecord error)
+ {
+ error = null;
+
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ bool result = false;
+
+ path = NormalizePath(path);
+
+ if (String.IsNullOrEmpty(path))
+ {
+ return true;
+ }
+ try
+ {
+ bool notUsed;
+ // Exception accessException;
+
+ // First see if the file exists
+ try {
+ if (ArchiveDriveInfo.ItemExists(path))
+ {
+ result = true;
+ }
+ }
+ catch (IOException ioException)
+ {
+ // File Archive Open and ArchiveItem Open throws the same errors, need to validate
+ // ArchiveItem existance.
+ if (ioException.Message != String.Format(ArchiveProviderStrings.ItemNotFound, path))
+ {
+ throw ioException;
+ }
+
+ }
+ catch (PSArgumentException psArgumentException)
+ {
+
+ }
+
+ FileSystemItemProviderDynamicParameters itemExistsDynamicParameters =
+ DynamicParameters as FileSystemItemProviderDynamicParameters;
+
+ // If the items see if we need to check the age of the file...
+ if (result && itemExistsDynamicParameters != null)
+ {
+ // DateTime lastWriteTime = File.GetLastWriteTime(path);
+
+ // if (itemExistsDynamicParameters.OlderThan.HasValue)
+ // {
+ // result = lastWriteTime < itemExistsDynamicParameters.OlderThan.Value;
+ // }
+ // if (itemExistsDynamicParameters.NewerThan.HasValue)
+ // {
+ // result = lastWriteTime > itemExistsDynamicParameters.NewerThan.Value;
+ // }
+ }
+ }
+ catch (System.Security.SecurityException security)
+ {
+ error = new ErrorRecord(security, "ItemExistsSecurityError", ErrorCategory.PermissionDenied, path);
+ }
+ catch (ArgumentException argument)
+ {
+ error = new ErrorRecord(argument, "ItemExistsArgumentError", ErrorCategory.InvalidArgument, path);
+ }
+ catch (UnauthorizedAccessException unauthorized)
+ {
+ error = new ErrorRecord(unauthorized, "ItemExistsUnauthorizedAccessError", ErrorCategory.PermissionDenied, path);
+ }
+ catch (PathTooLongException pathTooLong)
+ {
+ error = new ErrorRecord(pathTooLong, "ItemExistsPathTooLongError", ErrorCategory.InvalidArgument, path);
+ }
+ catch (NotSupportedException notSupported)
+ {
+ error = new ErrorRecord(notSupported, "ItemExistsNotSupportedError", ErrorCategory.InvalidOperation, path);
+ }
+
+ return result;
+ }
+
+ #endregion ItemExists
+ #region HasChildItems
+
+ ///
+ /// Determines if the given path is a directory, and has children.
+ ///
+ ///
+ /// The full path to the directory.
+ ///
+ ///
+ /// True if the path refers to a directory that contains other
+ /// directories or files. False otherwise.
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ protected override bool HasChildItems(string path)
+ {
+ bool result = false;
+
+ // verify parameters
+ if (string.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ return IsItemContainer(path) && IsItemContainerContainsItems(path);
+ }
+
+ #endregion HasChildItems
+ #region CopyItem
+ ///
+ /// Copies an item at the specified path to the given destination.
+ ///
+ ///
+ ///
+ /// The path of the item to copy.
+ ///
+ ///
+ ///
+ /// The path of the destination.
+ ///
+ ///
+ ///
+ /// Specifies if the operation should also copy child items.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ /// destination path is null or empty.
+ ///
+ ///
+ ///
+ /// Nothing. Copied items are written to the context's pipeline.
+ ///
+ protected override void CopyItem(
+ string path,
+ string destinationPath,
+ bool recurse)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ if (String.IsNullOrEmpty(destinationPath))
+ {
+ throw TraceSource.NewArgumentException("destinationPath");
+ }
+
+ path = NormalizePath(path);
+ destinationPath = NormalizePath(destinationPath);
+
+ // Clean up "newname" to fix some common usability problems:
+ // Rename .\foo.txt .\bar.txt
+ // Rename c:\temp\foo.txt c:\temp\bar.txt
+ if (destinationPath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) ||
+ destinationPath.StartsWith("./", StringComparison.OrdinalIgnoreCase))
+ {
+ destinationPath = destinationPath.Remove(0, 2);
+ }
+
+ bool pathIsDirectory = ArchiveDriveInfo.IsItemContainer(path);
+ bool destIsDirectory = false;
+
+ if (PathUtils.EndsInDirectorySeparator(destinationPath))
+ {
+ destIsDirectory = true;
+ }
+
+ // Check if wildcard exists and destination is not a directory.
+ // This should throw
+
+ //CopyItemDynamicParameters copyDynamicParameter = DynamicParameters as CopyItemDynamicParameters;
+
+ //if (copyDynamicParameter != null)
+ //{
+ // if (copyDynamicParameter.FromSession != null)
+ // {
+ // fromSession = copyDynamicParameter.FromSession;
+ // }
+ // else
+ // {
+ // toSession = copyDynamicParameter.ToSession;
+ // }
+ //}
+
+ // Wildcard Items dont exist.
+ try
+ {
+
+ IEnumerable files;
+ if (pathIsDirectory)
+ {
+ files = ArchiveDriveInfo.GetItem(path+"/*", true, true);
+ }
+ else
+ {
+ files = ArchiveDriveInfo.GetItem(path, true, true);
+ }
+
+ // Confirm the move with the user
+ string action = ArchiveProviderStrings.CopyItemActionFile;
+ foreach (ArchiveItemInfo file in files)
+ {
+ string driveName = (file.Drive.Name + Path.VolumeSeparatorChar + Path.DirectorySeparatorChar);
+
+ string resource = String.Format(ArchiveProviderStrings.CopyItemResourceFileTemplate, file.FullName, destinationPath);
+ if (ShouldProcess(resource, action))
+ {
+ // If pathIsDirectory
+ string destPath = destinationPath;
+
+ if (pathIsDirectory)
+ {
+ string relPath = Path.GetRelativePath($"{driveName}{path}", file.FullName);
+ destPath = Path.Join(destinationPath, relPath);
+ }
+ else if (destIsDirectory) {
+ destPath = Path.Join(destinationPath, file.Name);
+ }
+
+ file.CopyTo(destPath);
+ }
+
+ }
+ }
+ catch(Exception e) {
+ throw e;
+ }
+ }
+
+ #endregion CopyItem
+ #endregion ContainerCmdletProvider members
+ #region NavigationCmdletProvider members
+
+ // Note: we don't use IO.Path.IsPathRooted as this deals with "invalid" i.e. unnormalized paths
+ private static bool IsAbsolutePath(string path)
+ {
+ Console.WriteLine($"IsAbsolutePath: {path}");
+ return false;
+ }
+
+ internal static bool PathIsUnc(string path)
+ {
+#if UNIX
+ return false;
+#else
+ Uri uri;
+ return !string.IsNullOrEmpty(path) && Uri.TryCreate(path, UriKind.Absolute, out uri) && uri.IsUnc;
+#endif
+ }
+
+ protected bool IsItemContainerContainsItems(string path)
+ {
+ bool result = false;
+
+ if (!PathUtils.EndsInDirectorySeparator(path))
+ {
+ path += Path.DirectorySeparatorChar;
+ }
+ path += "*";
+
+ ArchiveItemInfo[] items = ArchiveDriveInfo.GetItem(path).ToArray();
+
+ if (items.Length > 0)
+ {
+ result = true;
+ }
+
+ return result;
+ }
+
+ protected override bool IsItemContainer(string path)
+ {
+ path = NormalizePath(path);
+
+ if ( String.IsNullOrEmpty(path) )
+ {
+ return true;
+ }
+ else if ( path == "\\" || path == "/")
+ {
+ return true;
+ }
+
+ return ArchiveDriveInfo.IsItemContainer(path);
+ }
+
+ #region MoveItem
+
+ #endregion MoveItem
+ #endregion NavigationCmdletProvider members
+ #region IPropertyCmdletProvider
+
+ #endregion IPropertyCmdletProvider
+ #region IContentCmdletProvider
+
+ ///
+ /// Creates an instance of the FileSystemContentStream class, opens
+ /// the specified file for reading, and returns the IContentReader interface
+ /// to it.
+ ///
+ ///
+ /// The path of the file to be opened for reading.
+ ///
+ ///
+ /// An IContentReader for the specified file.
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ public IContentReader GetContentReader(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ if (IsItemContainer(path))
+ {
+ throw new Exception("You cannot read the contents of a folder");
+ }
+
+ // Defaults for the file read operation
+ string delimiter = "\n";
+
+ Encoding encoding = Encoding.Default;
+ // Encoding encoding = new Encoding.Default();
+
+ bool streamTypeSpecified = false;
+ bool usingByteEncoding = false;
+ bool delimiterSpecified = false;
+ bool isRawStream = false;
+
+ // Get the dynamic parameters.
+ // They override the defaults specified above.
+ if (DynamicParameters != null)
+ {
+ StreamContentReaderDynamicParameters dynParams = DynamicParameters as StreamContentReaderDynamicParameters;
+ if (dynParams != null)
+ {
+ // -raw is not allowed when -first,-last or -wait is specified
+ // this call will validate that and throws.
+ ValidateParameters(dynParams.Raw);
+
+ isRawStream = dynParams.Raw;
+
+ // Get the delimiter
+ delimiterSpecified = dynParams.DelimiterSpecified;
+ if (delimiterSpecified)
+ delimiter = dynParams.Delimiter;
+
+ // Get the stream type
+ usingByteEncoding = dynParams.AsByteStream;
+ streamTypeSpecified = dynParams.WasStreamTypeSpecified;
+
+ if (usingByteEncoding && streamTypeSpecified)
+ {
+ WriteWarning(ArchiveProviderStrings.EncodingNotUsed);
+ }
+
+ if (streamTypeSpecified)
+ {
+ encoding = dynParams.Encoding;
+ }
+
+ }
+ }
+ StreamContentReaderWriter stream = null;
+
+ ArchiveItemInfo archiveFile = new ArchiveItemInfo(ArchiveDriveInfo, path);
+
+ try
+ {
+ // Users can't both read as bytes, and specify a delimiter
+ if (delimiterSpecified)
+ {
+ if (usingByteEncoding)
+ {
+ Exception e =
+ new ArgumentException(ArchiveProviderStrings.DelimiterError, "delimiter");
+ WriteError(new ErrorRecord(
+ e,
+ "GetContentReaderArgumentError",
+ ErrorCategory.InvalidArgument,
+ path));
+ }
+ else
+ {
+ stream = new ArchiveContentStream(archiveFile, FileMode.Append, delimiter, encoding, usingByteEncoding, this, isRawStream);
+ }
+ }
+ else
+ {
+ stream = new ArchiveContentStream(archiveFile, FileMode.Append, encoding, usingByteEncoding, this, isRawStream);
+ }
+ }
+ catch (PathTooLongException pathTooLong)
+ {
+ WriteError(new ErrorRecord(pathTooLong, "GetContentReaderPathTooLongError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (FileNotFoundException fileNotFound)
+ {
+ WriteError(new ErrorRecord(fileNotFound, "GetContentReaderFileNotFoundError", ErrorCategory.ObjectNotFound, path));
+ }
+ catch (DirectoryNotFoundException directoryNotFound)
+ {
+ WriteError(new ErrorRecord(directoryNotFound, "GetContentReaderDirectoryNotFoundError", ErrorCategory.ObjectNotFound, path));
+ }
+ catch (ArgumentException argException)
+ {
+ WriteError(new ErrorRecord(argException, "GetContentReaderArgumentError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (IOException ioException)
+ {
+ // IOException contains specific message about the error occured and so no need for errordetails.
+ WriteError(new ErrorRecord(ioException, "GetContentReaderIOError", ErrorCategory.ReadError, path));
+ }
+ catch (System.Security.SecurityException securityException)
+ {
+ WriteError(new ErrorRecord(securityException, "GetContentReaderSecurityError", ErrorCategory.PermissionDenied, path));
+ }
+ catch (UnauthorizedAccessException unauthorizedAccess)
+ {
+ WriteError(new ErrorRecord(unauthorizedAccess, "GetContentReaderUnauthorizedAccessError", ErrorCategory.PermissionDenied, path));
+ }
+ catch (Exception e)
+ {
+ WriteError(
+ new ErrorRecord(e, "Unhandled Error", ErrorCategory.InvalidArgument , path)
+ );
+ }
+
+ if (stream == null)
+ {
+ throw new Exception("Invalid stream");
+ }
+
+ return stream;
+ }
+
+ public object GetContentReaderDynamicParameters(string path)
+ {
+ return new StreamContentReaderDynamicParameters();
+ }
+
+ ///
+ /// Creates an instance of the FileSystemContentStream class, opens
+ /// the specified file for writing, and returns the IContentReader interface
+ /// to it.
+ ///
+ ///
+ /// The path of the file to be opened for writing.
+ ///
+ ///
+ /// An IContentWriter for the specified file.
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ public IContentWriter GetContentWriter(string path)
+ {
+
+ if (string.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ // If this is true, then the content will be read as bytes
+ bool usingByteEncoding = false;
+ bool streamTypeSpecified = false;
+
+ //Encoding encoding = ClrFacade.GetDefaultEncoding();
+ Encoding encoding = Encoding.Default;
+
+ FileMode filemode = FileMode.OpenOrCreate;
+ bool suppressNewline = false;
+
+ // Get the dynamic parameters
+ if (DynamicParameters != null)
+ {
+
+ StreamContentWriterDynamicParameters dynParams = DynamicParameters as StreamContentWriterDynamicParameters;
+
+ if (dynParams != null)
+ {
+ usingByteEncoding = dynParams.AsByteStream;
+ streamTypeSpecified = dynParams.WasStreamTypeSpecified;
+
+ if (usingByteEncoding && streamTypeSpecified)
+ {
+ WriteWarning(ArchiveProviderStrings.EncodingNotUsed);
+ }
+
+ if (streamTypeSpecified)
+ {
+ encoding = dynParams.Encoding;
+ }
+
+ suppressNewline = dynParams.NoNewline.IsPresent;
+ }
+ }
+
+ StreamContentReaderWriter stream = null;
+
+ // Validate Parent Directory does not exist
+ if (!IsItemContainer(Path.GetDirectoryName(path)))
+ {
+ throw new Exception("Parent directory does not exist");
+ }
+ if (IsItemContainer(path))
+ {
+ throw new Exception("You cannot write to a folder");
+ }
+
+ ArchiveItemInfo archiveFile = new ArchiveItemInfo(ArchiveDriveInfo, path, true);
+
+ try
+ {
+ stream = new ArchiveContentStream(archiveFile, FileMode.Append, encoding, usingByteEncoding, this, false, suppressNewline);
+ }
+ catch (PathTooLongException pathTooLong)
+ {
+ WriteError(new ErrorRecord(pathTooLong, "GetContentWriterPathTooLongError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (FileNotFoundException fileNotFound)
+ {
+ WriteError(new ErrorRecord(fileNotFound, "GetContentWriterFileNotFoundError", ErrorCategory.ObjectNotFound, path));
+ }
+ catch (DirectoryNotFoundException directoryNotFound)
+ {
+ WriteError(new ErrorRecord(directoryNotFound, "GetContentWriterDirectoryNotFoundError", ErrorCategory.ObjectNotFound, path));
+ }
+ catch (ArgumentException argException)
+ {
+ WriteError(new ErrorRecord(argException, "GetContentWriterArgumentError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (IOException ioException)
+ {
+ // IOException contains specific message about the error occured and so no need for errordetails.
+ WriteError(new ErrorRecord(ioException, "GetContentWriterIOError", ErrorCategory.WriteError, path));
+ }
+ catch (System.Security.SecurityException securityException)
+ {
+ WriteError(new ErrorRecord(securityException, "GetContentWriterSecurityError", ErrorCategory.PermissionDenied, path));
+ }
+ catch (UnauthorizedAccessException unauthorizedAccess)
+ {
+ WriteError(new ErrorRecord(unauthorizedAccess, "GetContentWriterUnauthorizedAccessError", ErrorCategory.PermissionDenied, path));
+ }
+
+ return stream;
+ }
+
+ public object GetContentWriterDynamicParameters(string path)
+ {
+ return new StreamContentWriterDynamicParameters();
+ }
+
+ ///
+ /// Clears the content of the specified file.
+ ///
+ ///
+ ///
+ /// The path to the file of which to clear the contents.
+ ///
+ ///
+ ///
+ /// path is null or empty.
+ ///
+ public void ClearContent(string path)
+ {
+
+ if (String.IsNullOrEmpty(path))
+ {
+ throw TraceSource.NewArgumentException("path");
+ }
+
+ path = NormalizePath(path);
+
+ try
+ {
+ bool clearStream = false;
+ string streamName = null;
+ FileSystemClearContentDynamicParameters dynamicParameters = null;
+ FileSystemContentWriterDynamicParameters writerDynamicParameters = null;
+
+ // We get called during:
+ // - Clear-Content
+ // - Set-Content, in the phase that clears the path first.
+ if (DynamicParameters != null)
+ {
+ dynamicParameters = DynamicParameters as FileSystemClearContentDynamicParameters;
+ writerDynamicParameters = DynamicParameters as FileSystemContentWriterDynamicParameters;
+ }
+
+ string action = ArchiveProviderStrings.ClearContentActionFile;
+ string resource = String.Format(ArchiveProviderStrings.ClearContentesourceTemplate, path);
+
+ if (!ShouldProcess(resource, action))
+ return;
+
+ // Validate Parent Directory does not exist
+ if (!IsItemContainer(Path.GetDirectoryName(path)))
+ {
+ throw new Exception("Parent directory does not exist");
+ }
+
+ path = NormalizePath(path);
+
+ ArchiveItemInfo archiveFile = new ArchiveItemInfo(ArchiveDriveInfo, path, Force.ToBool());
+ archiveFile.ClearContent();
+
+ // For filesystem once content is cleared
+ WriteItemObject("", path, false);
+ }
+ catch (ArgumentException argException)
+ {
+ WriteError(new ErrorRecord(argException, "ClearContentArgumentError", ErrorCategory.InvalidArgument, path));
+ }
+ catch (FileNotFoundException fileNotFoundException)
+ {
+ WriteError(new ErrorRecord(fileNotFoundException, "PathNotFound", ErrorCategory.ObjectNotFound, path));
+ }
+ catch (IOException ioException)
+ {
+ //IOException contains specific message about the error occured and so no need for errordetails.
+ WriteError(new ErrorRecord(ioException, "ClearContentIOError", ErrorCategory.WriteError, path));
+ }
+ }
+
+ public object ClearContentDynamicParameters(string path)
+ {
+ return new StreamContentClearContentDynamicParameters();
+ }
+ #endregion IContentCmdletProvider
+
+ ///
+ /// -raw is not allowed when -first,-last or -wait is specified
+ /// this call will validate that and throws.
+ ///
+ private void ValidateParameters(bool isRawSpecified)
+ {
+ if (isRawSpecified)
+ {
+ if (this.Context_MyInvocation.BoundParameters.ContainsKey("TotalCount"))
+ {
+ string message = String.Format(ArchiveProviderStrings.NoFirstLastWaitForRaw, "Raw", "TotalCount");
+ throw new PSInvalidOperationException(message);
+ }
+
+
+ if (this.Context_MyInvocation.BoundParameters.ContainsKey("Tail"))
+ {
+ string message = String.Format(ArchiveProviderStrings.NoFirstLastWaitForRaw, "Raw", "Tail");
+ throw new PSInvalidOperationException(message);
+ }
+
+ if (this.Context_MyInvocation.BoundParameters.ContainsKey("Delimiter"))
+ {
+ string message = String.Format(ArchiveProviderStrings.NoFirstLastWaitForRaw, "Raw", "Delimiter");
+ throw new PSInvalidOperationException(message);
+ }
+ }
+ }
+
+ #region InodeTracker
+ private HashSet<(UInt64, UInt64)> _visitations;
+ #endregion
+ #endregion
+ #region Dynamic Parameters
+
+ #endregion
+ #region Symbolic Link
+
+ #endregion
+ #region AlternateDataStreamUtilities
+
+ #endregion
+ #region CopyFileFromRemoteUtils
+
+ #region PSCopyToSessionHelper
+
+ #endregion
+ #region PSCopyFromSessionHelper
+
+ #endregion
+ #region PSCopyRemoteUtils
+
+ #endregion
+ #endregion
+ }
+ #endregion ArchiveProvider
+}
diff --git a/src/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.csproj b/src/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.csproj
new file mode 100644
index 0000000..047febe
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.csproj
@@ -0,0 +1,34 @@
+
+
+
+ netcoreapp3.1
+ linux-x64;osx-x64;win;
+ Microsoft.PowerShell.Archive
+ true
+
+
+
+
+
+
+
+
+ Microsoft.PowerShell.Archive
+ $(MSBuildProjectDirectory)$(OutDir)
+ $(MSBuildStartupDirectory)\$(PSModuleName)\bin\
+ $([System.IO.Path]::GetFullPath(`$(MSBuildProjectDirectory)\..\ResGen`))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.PowerShell.Archive/resources/ArchiveProviderStrings.resx b/src/Microsoft.PowerShell.Archive/resources/ArchiveProviderStrings.resx
new file mode 100644
index 0000000..185ee7a
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/resources/ArchiveProviderStrings.resx
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ GetChildItems
+
+
+ Path contains invalid characters.
+
+
+ Create File
+
+
+ Create Directory
+
+
+ Destination: {0}
+
+
+ Clear Content
+
+
+ Item: {0}
+
+
+ Could not find item {0}.
+
+
+ An object at the specified path {0} does not exist.
+
+
+ You do not have sufficient access rights to perform this operation or the item is hidden, system, or read only.
+
+
+ The '{0}' and '{1}' parameters cannot be specified in the same command.
+
+
+ Encoding not used when '-AsByteStream' specified.
+
+
+ A delimiter cannot be specified when reading the stream one byte at a time.
+
+
+
+ Cannot rename the specified target, because it represents a path or device name.
+
+
+ Rename File
+
+
+ Item: {0} Destination: {1}
+
+
+
+ Copy File
+
+
+ Item: {0} Destination: {1}
+
+
+ Copy Directory
+
+
+
+ Invoke Item
+
+
+ Item: {0}
+
+
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/resources/Exceptions.resx b/src/Microsoft.PowerShell.Archive/resources/Exceptions.resx
new file mode 100644
index 0000000..b296c0e
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/resources/Exceptions.resx
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Cannot process argument because the value of argument "{0}" is not valid. Change the value of the "{0}" argument and run the operation again.
+
+
+
+ Cannot process argument because the value of argument "{0}" is null. Change the value of argument "{0}" to a non-null value.
+
+
+
+ Cannot process argument because the value of argument "{0}" is out of range. Change argument "{0}" to a value that is within range.
+
+
+
+ Cannot perform operation because operation "{0}" is not supported.
+
+
+
+ Cannot detect the encoding of the file. The specified encoding {0} is not supported when the content is read in reverse.
+
+
+ Cannot proceed with byte encoding. When using byte encoding the content must be of type byte.
+
+
+ Unknown encoding {0}; valid values are {1}.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/resources/Microsoft.PowerShell.Commands.ArchiveFileInfo.Format.ps1xml b/src/Microsoft.PowerShell.Archive/resources/Microsoft.PowerShell.Commands.ArchiveFileInfo.Format.ps1xml
new file mode 100644
index 0000000..48a92df
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/resources/Microsoft.PowerShell.Commands.ArchiveFileInfo.Format.ps1xml
@@ -0,0 +1,44 @@
+
+
+
+
+ Microsoft.PowerShell.Commands.ZipFileItemInfo
+
+ Microsoft.PowerShell.Commands.ZipFileItemInfo
+
+
+
+
+ 25
+
+
+ 8
+
+
+ 25
+
+
+
+
+
+
+
+
+ LastWriteTime
+
+
+ Length
+
+
+ Name
+
+
+ FullName
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/utils/Assert.cs b/src/Microsoft.PowerShell.Archive/utils/Assert.cs
new file mode 100644
index 0000000..ebebde3
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/Assert.cs
@@ -0,0 +1,204 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+// The define below is only valid for this file. It allows the methods
+// defined here to call Diagnostics.Assert when only ASSERTIONS_TRACE is defined
+// Any #if DEBUG is pointless (always true) in this file because of this declaration.
+// The presence of the define will cause the System.Diagnostics.Debug.Asser calls
+// always to be compiled in for this file. What can be compiled out are the calls to
+// System.Management.Automation.Diagnostics.Assert in other files when neither DEBUG
+// nor ASSERTIONS_TRACE is defined.
+#define DEBUG
+using System;
+using System.Diagnostics;
+using System.Text;
+
+namespace Microsoft.PowerShell.Archive
+{
+ ///
+ /// Exception with a full stack trace excluding the last two frames.
+ ///
+ internal class AssertException : SystemException
+ {
+ ///
+ /// Calls the base class with message and sets the stack frame.
+ ///
+ /// Repassed to the base class.
+ internal AssertException(string message) : base(message)
+ {
+ // 3 will skip the assertion caller, this method and AssertException.StackTrace
+ StackTrace = Diagnostics.StackTrace(3);
+ }
+
+ ///
+ /// Returns the stack trace set in the constructor.
+ ///
+ /// the constructor's stackTrace
+ public override string StackTrace { get; }
+ }
+
+ ///
+ /// This class contain the few methods necessary for
+ /// the basic assertion use.
+ ///
+ ///
+ /// All methods are public and static.
+ /// The class cannot derive from the sealed System.Diagnostics.Debug
+ /// The class was also made sealed.
+ ///
+ ///
+ ///
+ /// Diagnostics.Assert(x >= 0,"A negative x would have caused early return.");
+ ///
+ ///
+ ///
+ ///
+ internal sealed class Diagnostics
+ {
+ internal static string StackTrace(int framesToSkip)
+ {
+ StackTrace trace = new StackTrace(true);
+ StackFrame[] frames = trace.GetFrames();
+ StringBuilder frameString = new StringBuilder();
+ int maxFrames = 10;
+ maxFrames += framesToSkip;
+ for (int i = framesToSkip; (i < frames.Length) && (i < maxFrames); i++)
+ {
+ StackFrame frame = frames[i];
+ frameString.Append(frame.ToString());
+ }
+
+ return frameString.ToString();
+ }
+
+ private static object s_throwInsteadOfAssertLock = 1;
+
+ private static bool s_throwInsteadOfAssert = false;
+ ///
+ /// If set to true will prevent the assertion dialog from showing up
+ /// by throwing an exception instead of calling Debug.Assert.
+ ///
+ /// false for dialog, true for exception
+ internal static bool ThrowInsteadOfAssert
+ {
+ get
+ {
+ lock (s_throwInsteadOfAssertLock)
+ {
+ return s_throwInsteadOfAssert;
+ }
+ }
+
+ set
+ {
+ lock (s_throwInsteadOfAssertLock)
+ {
+ s_throwInsteadOfAssert = value;
+ }
+ }
+ }
+
+ ///
+ /// This class only has statics, so we shouldn't need to instantiate any object.
+ ///
+ private Diagnostics() { }
+
+ ///
+ /// Basic assertion with logical condition and message.
+ ///
+ ///
+ /// logical condition that should be true for program to proceed
+ ///
+ ///
+ /// Message to explain why condition should always be true
+ ///
+ // These two lines are playing havoc with asmmeta. Since only one asmmeta file
+ // can be checked in at a time if you compile the asmmeta for a fre build then
+ // the checked can't compile against it since these methods will not exist. If
+ // you check in the chk asmmeta the fre build will not compile because it is
+ // not expecting these methods to exist.
+ [System.Diagnostics.Conditional("DEBUG")]
+ [System.Diagnostics.Conditional("ASSERTIONS_TRACE")]
+#if RESHARPER_ATTRIBUTES
+ [JetBrains.Annotations.AssertionMethod]
+#endif
+ internal static void Assert(
+#if RESHARPER_ATTRIBUTES
+ [JetBrains.Annotations.AssertionCondition(JetBrains.Annotations.AssertionConditionType.IS_TRUE)]
+#endif
+ bool condition,
+ string whyThisShouldNeverHappen)
+ {
+ Diagnostics.Assert(condition, whyThisShouldNeverHappen, string.Empty);
+ }
+
+ ///
+ /// Basic assertion with logical condition, message and detailed message.
+ ///
+ ///
+ /// logical condition that should be true for program to proceed
+ ///
+ ///
+ /// Message to explain why condition should always be true
+ ///
+ ///
+ /// Additional information about the assertion
+ ///
+ // These two lines are playing havoc with asmmeta. Since only one asmmeta file
+ // can be checked in at a time if you compile the asmmeta for a fre build then
+ // the checked can't compile against it since these methods will not exist. If
+ // you check in the chk asmmeta the fre build will not compile because it is
+ // not expecting these methods to exist.
+ [System.Diagnostics.Conditional("DEBUG")]
+ [System.Diagnostics.Conditional("ASSERTIONS_TRACE")]
+#if RESHARPER_ATTRIBUTES
+ [JetBrains.Annotations.AssertionMethod]
+#endif
+ internal static void
+ Assert(
+#if RESHARPER_ATTRIBUTES
+ [JetBrains.Annotations.AssertionCondition(JetBrains.Annotations.AssertionConditionType.IS_TRUE)]
+#endif
+ bool condition,
+ string whyThisShouldNeverHappen, string detailMessage)
+ {
+ // Early out avoids some slower code below (mostly the locking done in ThrowInsteadOfAssert).
+ if (condition) return;
+
+#if ASSERTIONS_TRACE
+ if (!condition)
+ {
+ if (Diagnostics.ThrowInsteadOfAssert)
+ {
+ string assertionMessage = "ASSERT: " + whyThisShouldNeverHappen + " " + detailMessage + " ";
+ AssertException e = new AssertException(assertionMessage);
+ tracer.TraceException(e);
+ throw e;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.Append("ASSERT: ");
+ builder.Append(whyThisShouldNeverHappen);
+ builder.Append(".");
+ if (detailMessage.Length != 0)
+ {
+ builder.Append(detailMessage);
+ builder.Append(".");
+ }
+ // 2 to skip this method and Diagnostics.StackTace
+ builder.Append(Diagnostics.StackTrace(2));
+ tracer.TraceError(builder.ToString());
+ }
+#else
+ if (Diagnostics.ThrowInsteadOfAssert)
+ {
+ string assertionMessage = "ASSERT: " + whyThisShouldNeverHappen + " " + detailMessage + " ";
+ throw new AssertException(assertionMessage);
+ }
+
+ System.Diagnostics.Debug.Fail(whyThisShouldNeverHappen, detailMessage);
+#endif
+ }
+ }
+}
+
diff --git a/src/Microsoft.PowerShell.Archive/utils/CorePsPlatform.cs b/src/Microsoft.PowerShell.Archive/utils/CorePsPlatform.cs
new file mode 100644
index 0000000..d0f4f99
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/CorePsPlatform.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Runtime.InteropServices;
+
+using Microsoft.Win32;
+//using Microsoft.Win32.Registry;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.PowerShell.Archive
+{
+
+ ///
+ /// These are platform abstractions and platform specific implementations.
+ ///
+ public static class Platform
+ {
+ private static string _tempDirectory = null;
+
+ ///
+ /// True if the current platform is Linux.
+ ///
+ public static bool IsLinux
+ {
+ get
+ {
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ }
+ }
+
+ ///
+ /// True if the current platform is macOS.
+ ///
+ public static bool IsMacOS
+ {
+ get
+ {
+ return RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ }
+ }
+
+ ///
+ /// True if the current platform is Windows.
+ ///
+ public static bool IsWindows
+ {
+ get
+ {
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ }
+ }
+
+ ///
+ /// True if the underlying system is NanoServer.
+ ///
+ public static bool IsNanoServer
+ {
+ get
+ {
+#if UNIX
+ return false;
+#else
+ if (_isNanoServer.HasValue) { return _isNanoServer.Value; }
+
+ _isNanoServer = false;
+ using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels"))
+ {
+ if (regKey != null)
+ {
+ object value = regKey.GetValue("NanoServer");
+ if (value != null && regKey.GetValueKind("NanoServer") == RegistryValueKind.DWord)
+ {
+ _isNanoServer = (int)value == 1;
+ }
+ }
+ }
+
+ return _isNanoServer.Value;
+#endif
+ }
+ }
+
+ ///
+ /// True if the underlying system is IoT.
+ ///
+ public static bool IsIoT
+ {
+ get
+ {
+#if UNIX
+ return false;
+#else
+ if (_isIoT.HasValue) { return _isIoT.Value; }
+
+ _isIoT = false;
+ using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"))
+ {
+ if (regKey != null)
+ {
+ object value = regKey.GetValue("ProductName");
+ if (value != null && regKey.GetValueKind("ProductName") == RegistryValueKind.String)
+ {
+ _isIoT = string.Equals("IoTUAP", (string)value, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+
+ return _isIoT.Value;
+#endif
+ }
+ }
+
+ ///
+ /// True if underlying system is Windows Desktop.
+ ///
+ public static bool IsWindowsDesktop
+ {
+ get
+ {
+#if UNIX
+ return false;
+#else
+ if (_isWindowsDesktop.HasValue) { return _isWindowsDesktop.Value; }
+
+ _isWindowsDesktop = !IsNanoServer && !IsIoT;
+ return _isWindowsDesktop.Value;
+#endif
+ }
+ }
+
+#if UNIX
+ // Gets the location for cache and config folders.
+ internal static readonly string CacheDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE);
+ internal static readonly string ConfigDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CONFIG);
+#else
+ // Gets the location for cache and config folders.
+ internal static readonly string CacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell";
+ internal static readonly string ConfigDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + @"\PowerShell";
+
+ private static bool? _isNanoServer = null;
+ private static bool? _isIoT = null;
+ private static bool? _isWindowsDesktop = null;
+#endif
+ }
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/utils/EncodingUtils.cs b/src/Microsoft.PowerShell.Archive/utils/EncodingUtils.cs
new file mode 100644
index 0000000..926c5cd
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/EncodingUtils.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Management.Automation;
+using System.Management.Automation.Internal;
+
+namespace Microsoft.PowerShell.Archive
+{
+ internal static class EncodingConversion
+ {
+ internal const string Unknown = "unknown";
+ internal const string String = "string";
+ internal const string Unicode = "unicode";
+ internal const string BigEndianUnicode = "bigendianunicode";
+ internal const string Ascii = "ascii";
+ internal const string Utf8 = "utf8";
+ internal const string Utf8NoBom = "utf8NoBOM";
+ internal const string Utf8Bom = "utf8BOM";
+ internal const string Utf7 = "utf7";
+ internal const string Utf32 = "utf32";
+ internal const string Default = "default";
+ internal const string OEM = "oem";
+ internal static readonly string[] TabCompletionResults = {
+ Ascii, BigEndianUnicode, OEM, Unicode, Utf7, Utf8, Utf8Bom, Utf8NoBom, Utf32
+ };
+
+ internal static Dictionary encodingMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { Ascii, System.Text.Encoding.ASCII },
+ { BigEndianUnicode, System.Text.Encoding.BigEndianUnicode },
+ { Default, System.Text.Encoding.Default },
+ { OEM, System.Text.Encoding.Default },
+ { Unicode, System.Text.Encoding.Unicode },
+ { Utf7, System.Text.Encoding.UTF7 },
+ { Utf8, System.Text.Encoding.UTF8 },
+ { Utf8Bom, System.Text.Encoding.UTF8 },
+ { Utf8NoBom, System.Text.Encoding.UTF8 },
+ { Utf32, System.Text.Encoding.UTF32 },
+ { String, System.Text.Encoding.Unicode },
+ { Unknown, System.Text.Encoding.Unicode },
+ };
+
+ ///
+ /// Retrieve the encoding parameter from the command line
+ /// it throws if the encoding does not match the known ones.
+ ///
+ /// A System.Text.Encoding object (null if no encoding specified).
+ internal static Encoding Convert(Cmdlet cmdlet, string encoding)
+ {
+ if (string.IsNullOrEmpty(encoding))
+ {
+ // no parameter passed, default to UTF8
+ return new UTF8Encoding(false);
+ }
+
+ Encoding foundEncoding;
+ if (encodingMap.TryGetValue(encoding, out foundEncoding))
+ {
+ return foundEncoding;
+ }
+
+ // error condition: unknown encoding value
+ string validEncodingValues = string.Join(", ", TabCompletionResults);
+ string msg = String.Format(Exceptions.OutFile_WriteToFileEncodingUnknown, encoding, validEncodingValues);
+
+ ErrorRecord errorRecord = new ErrorRecord(
+ TraceSource.NewArgumentException("Encoding"),
+ "WriteToFileEncodingUnknown",
+ ErrorCategory.InvalidArgument,
+ null);
+
+ errorRecord.ErrorDetails = new ErrorDetails(msg);
+ cmdlet.ThrowTerminatingError(errorRecord);
+
+ return null;
+ }
+ }
+
+ ///
+ /// To make it easier to specify -Encoding parameter, we add an ArgumentTransformationAttribute here.
+ /// When the input data is of type string and is valid to be converted to System.Text.Encoding, we do
+ /// the conversion and return the converted value. Otherwise, we just return the input data.
+ ///
+ internal sealed class ArgumentToEncodingTransformationAttribute : ArgumentTransformationAttribute
+ {
+ public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
+ {
+ switch (inputData)
+ {
+ case string stringName:
+ if (EncodingConversion.encodingMap.TryGetValue(stringName, out Encoding foundEncoding))
+ {
+ return foundEncoding;
+ }
+ else
+ {
+ return System.Text.Encoding.GetEncoding(stringName);
+ }
+ case int intName:
+ return System.Text.Encoding.GetEncoding(intName);
+ }
+
+ return inputData;
+ }
+ }
+
+ ///
+ /// Provides the set of Encoding values for tab completion of an Encoding parameter.
+ ///
+ internal sealed class ArgumentEncodingCompletionsAttribute : ArgumentCompletionsAttribute
+ {
+ public ArgumentEncodingCompletionsAttribute() : base(
+ EncodingConversion.Ascii,
+ EncodingConversion.BigEndianUnicode,
+ EncodingConversion.OEM,
+ EncodingConversion.Unicode,
+ EncodingConversion.Utf7,
+ EncodingConversion.Utf8,
+ EncodingConversion.Utf8Bom,
+ EncodingConversion.Utf8NoBom,
+ EncodingConversion.Utf32
+ )
+ { }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/utils/ExtensibleCompletion.cs b/src/Microsoft.PowerShell.Archive/utils/ExtensibleCompletion.cs
new file mode 100644
index 0000000..cc5edd2
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/ExtensibleCompletion.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Management.Automation.Language;
+using System.Management.Automation;
+
+
+
+namespace Microsoft.PowerShell.Archive
+{
+ ///
+ /// This attribute is used to specify an argument completions for a parameter of a cmdlet or function
+ /// based on string array.
+ ///
+ /// [Parameter()]
+ /// [ArgumentCompletions("Option1","Option2","Option3")]
+ /// public string Noun { get; set; }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ public class ArgumentCompletionsAttribute : Attribute
+ {
+ private string[] _completions;
+
+ ///
+ /// Initializes a new instance of the ArgumentCompletionsAttribute class.
+ ///
+ /// List of complete values.
+ /// For null arguments.
+ /// For invalid arguments.
+ public ArgumentCompletionsAttribute(params string[] completions)
+ {
+ if (completions == null)
+ {
+ throw TraceSource.NewArgumentNullException("completions");
+ }
+
+ if (completions.Length == 0)
+ {
+ throw TraceSource.NewArgumentOutOfRangeException("completions", completions);
+ }
+
+ _completions = completions;
+ }
+
+ ///
+ /// The function returns completions for arguments.
+ ///
+ public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters)
+ {
+ var wordToCompletePattern = WildcardPattern.Get(string.IsNullOrWhiteSpace(wordToComplete) ? "*" : wordToComplete + "*", WildcardOptions.IgnoreCase);
+
+ foreach (var str in _completions)
+ {
+ if (wordToCompletePattern.IsMatch(str))
+ {
+ yield return new CompletionResult(str, str, CompletionResultType.ParameterValue, str);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/utils/PinvokeDllNames.cs b/src/Microsoft.PowerShell.Archive/utils/PinvokeDllNames.cs
new file mode 100644
index 0000000..1d76e86
Binary files /dev/null and b/src/Microsoft.PowerShell.Archive/utils/PinvokeDllNames.cs differ
diff --git a/src/Microsoft.PowerShell.Archive/utils/StreamContent.cs b/src/Microsoft.PowerShell.Archive/utils/StreamContent.cs
new file mode 100644
index 0000000..7fb32b1
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/StreamContent.cs
@@ -0,0 +1,1134 @@
+
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Management.Automation;
+using System.Management.Automation.Internal;
+using System.Management.Automation.Provider;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+using Dbg = System.Management.Automation;
+
+namespace Microsoft.PowerShell.Archive
+{
+ ///
+ /// The content stream base class for the Stream provider. It Implements both
+ /// the IContentReader and IContentWriter interfaces.
+ ///
+ ///
+ /// Note, this class does no specific error handling. All errors are allowed to
+ /// propagate to the caller so that they can be written to the error pipeline
+ /// if necessary.
+ ///
+ public class StreamContentReaderWriter : IContentReader, IContentWriter
+ {
+ private Encoding _encoding;
+ private CmdletProvider _provider;
+ private Stream _stream;
+ private StreamReader _reader;
+ private StreamWriter _writer;
+ private bool _usingByteEncoding;
+ private const char DefaultDelimiter = '\n';
+ private string _delimiter = $"{DefaultDelimiter}";
+ private int[] _offsetDictionary;
+ private bool _usingDelimiter;
+ private StringBuilder _currentLineContent;
+ private bool _isRawStream;
+ private long _fileOffset;
+
+ // The reader to read stream content backward
+ private StreamContentBackReader _backReader;
+
+ private bool _alreadyDetectEncoding = false;
+
+ // False to add a newline to the end of the output string, true if not.
+ private bool _suppressNewline = false;
+
+ ///
+ /// Constructor for the content stream.
+ ///
+ public StreamContentReaderWriter(System.IO.Stream stream, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream)
+ {
+ _encoding = encoding;
+ _usingByteEncoding = usingByteEncoding;
+ _provider = provider;
+ _isRawStream = isRawStream;
+
+ CreateStreams(stream, encoding);
+ }
+
+ ///
+ /// Constructor for the content stream.
+ ///
+ public StreamContentReaderWriter(System.IO.Stream stream, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream, bool suppressNewline)
+ : this(stream, encoding, usingByteEncoding, provider, isRawStream)
+ {
+
+ _suppressNewline = suppressNewline;
+ }
+
+ ///
+ /// Constructor for the content stream.
+ ///
+ ///
+ /// The name of the Alternate Data Stream to get the content from. If null or empty, returns
+ /// the file's primary content.
+ ///
+ ///
+ /// The delimiter to use when reading strings. Each time read is called, all contents up to an including
+ /// the delimiter is read.
+ ///
+ ///
+ /// The encoding of the file to be read or written.
+ ///
+ ///
+ /// The CmdletProvider invoking this stream
+ ///
+ ///
+ /// Indicates raw stream.
+ ///
+
+
+ public StreamContentReaderWriter(
+ System.IO.Stream stream,
+ string delimiter,
+ Encoding encoding,
+ CmdletProvider provider,
+ bool isRawStream)
+ : this(stream, encoding, false, provider, isRawStream)
+ {
+ // If the delimiter is default ('\n') we'll use ReadLine() method.
+ // Otherwise allocate temporary structures for ReadDelimited() method.
+ if (!(delimiter.Length == 1 && delimiter[0] == DefaultDelimiter))
+ {
+ _delimiter = delimiter;
+ _usingDelimiter = true;
+
+ // We expect that we are parsing files where line lengths can be relatively long.
+ const int DefaultLineLength = 256;
+ _currentLineContent = new StringBuilder(DefaultLineLength);
+
+ // For Boyer-Moore string search algorithm.
+ // Populate the offset lookups.
+ // These will tell us the maximum number of characters
+ // we can read to generate another possible match (safe shift).
+ // If we read more characters than this, we risk consuming
+ // more of the stream than we need.
+ //
+ // Because an unicode character size is 2 byte we would to have use
+ // very large array with 65535 size to keep this safe offsets.
+ // One solution is to pack unicode character to byte.
+ // The workaround is to use low byte from unicode character.
+ // This allow us to use small array with size 256.
+ // This workaround is the fastest and provides excellent results
+ // in regular search scenarios when the file contains
+ // mostly characters from the same alphabet.
+ _offsetDictionary = new int[256];
+
+ // If next char from file is not in search pattern safe shift is the search pattern length.
+ for (var n = 0; n < _offsetDictionary.Length; n++)
+ {
+ _offsetDictionary[n] = _delimiter.Length;
+ }
+
+ // If next char from file is in search pattern we should calculate a safe shift.
+ char currentChar;
+ byte lowByte;
+ for (var i = 0; i < _delimiter.Length; i++)
+ {
+ currentChar = _delimiter[i];
+ lowByte = Unsafe.As(ref currentChar);
+ _offsetDictionary[lowByte] = _delimiter.Length - i - 1;
+ }
+ }
+ }
+
+ ///
+ /// Reads the specified number of characters or a lines from the Stream.
+ ///
+ ///
+ /// If less than 1, then the entire Stream is read at once. If 1 or greater, then
+ /// readCount is used to determine how many items (ie: lines, bytes, delimited tokens)
+ /// to read per call.
+ ///
+ ///
+ /// An array of strings representing the character(s) or line(s) read from
+ /// the Stream.
+ ///
+ public IList Read(long readCount)
+ {
+ //s_tracer.WriteLine("blocks requested = {0}", readCount);
+
+ ArrayList blocks = new ArrayList();
+ bool readToEnd = (readCount <= 0);
+ bool waitChanges = false;
+
+ if (_alreadyDetectEncoding && _reader.BaseStream.Position == 0)
+ {
+ Encoding curEncoding = _reader.CurrentEncoding;
+ // Close the stream, and reopen the stream to make the BOM correctly processed.
+ // The reader has already detected encoding, so if we don't reopen the stream, the BOM (if there is any)
+ // will be treated as a regular character.
+ // _stream.Dispose();
+ CreateStreams(_stream, curEncoding);
+ _alreadyDetectEncoding = false;
+ }
+
+ try
+ {
+ for (long currentBlock = 0; (currentBlock < readCount) || (readToEnd); ++currentBlock)
+ {
+
+ if (_usingByteEncoding)
+ {
+ if (!ReadByteEncoded(waitChanges, blocks, false))
+ break;
+ }
+ else
+ {
+ if (_usingDelimiter || _isRawStream)
+ {
+ if (!ReadDelimited(waitChanges, blocks, false, _delimiter))
+ break;
+ }
+ else
+ {
+ if (!ReadByLine(waitChanges, blocks, false))
+ break;
+ }
+ }
+ }
+
+ //s_tracer.WriteLine("blocks read = {0}", blocks.Count);
+ }
+ catch (Exception e)
+ {
+ if ((e is IOException) ||
+ (e is ArgumentException) ||
+ (e is System.Security.SecurityException) ||
+ (e is UnauthorizedAccessException) ||
+ (e is ArgumentNullException))
+ {
+ // Exception contains specific message about the error occured and so no need for errordetails.
+ _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, "System.IO.Stream"));
+ return null;
+ }
+ else
+ throw;
+ }
+
+ return blocks.ToArray();
+ }
+
+ ///
+ /// Move the pointer of the stream to the position where there are 'backCount' number
+ /// of items (depends on what we are using: delimiter? line? byts?) to the end of the stream.
+ ///
+ ///
+ internal void SeekItemsBackward(int backCount)
+ {
+ if (backCount < 0)
+ {
+ // The caller needs to guarantee that 'backCount' is greater or equals to 0
+ throw TraceSource.NewArgumentException("backCount");
+ }
+
+ //s_tracer.WriteLine("blocks seek backwards = {0}", backCount);
+
+ ArrayList blocks = new ArrayList();
+ if (_reader != null)
+ {
+ // Make the reader automatically detect the encoding
+ Seek(0, SeekOrigin.Begin);
+ _reader.Peek();
+ _alreadyDetectEncoding = true;
+ }
+
+ Seek(0, SeekOrigin.End);
+
+ if (backCount == 0)
+ {
+ // If backCount is 0, we should move the position to the end of the stream.
+ // Maybe the "waitForChanges" is true in this case, which means that we are waiting for new inputs.
+ return;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ foreach (char character in _delimiter)
+ {
+ builder.Insert(0, character);
+ }
+
+ string actualDelimiter = builder.ToString();
+ long currentBlock = 0;
+ string lastDelimiterMatch = null;
+
+ try
+ {
+ if (_isRawStream)
+ {
+ // We always read to the end for the raw data.
+ // If it's indicated as RawStream, we move the pointer to the
+ // beginning of the stream
+ Seek(0, SeekOrigin.Begin);
+ return;
+ }
+
+ for (; currentBlock < backCount; ++currentBlock)
+ {
+ if (_usingByteEncoding)
+ {
+ if (!ReadByteEncoded(false, blocks, true))
+ break;
+ }
+ else
+ {
+ if (_usingDelimiter)
+ {
+ if (!ReadDelimited(false, blocks, true, actualDelimiter))
+ break;
+ // If the delimiter is at the end of the stream, we need to read one more
+ // to get to the right position. For example:
+ // ua123ua456ua -- -Tail 1
+ // If we read backward only once, we get 'ua', and cannot get to the right position
+ // So we read one more time, get 'ua456ua', and then we can get the right position
+ lastDelimiterMatch = (string)blocks[0];
+ if (currentBlock == 0 && lastDelimiterMatch.Equals(actualDelimiter, StringComparison.Ordinal))
+ backCount++;
+ }
+ else
+ {
+ if (!ReadByLine(false, blocks, true))
+ break;
+ }
+ }
+
+ blocks.Clear();
+ }
+
+ // If usingByteEncoding is true, we don't create the reader and _backReader
+ if (!_usingByteEncoding)
+ {
+ long curStreamPosition = _backReader.GetCurrentPosition();
+ if (_usingDelimiter)
+ {
+ if (currentBlock == backCount)
+ {
+ Diagnostics.Assert(lastDelimiterMatch != null, "lastDelimiterMatch should not be null when currentBlock == backCount");
+ if (lastDelimiterMatch.EndsWith(actualDelimiter, StringComparison.Ordinal))
+ {
+ curStreamPosition += _backReader.GetByteCount(_delimiter);
+ }
+ }
+ }
+
+ Seek(curStreamPosition, SeekOrigin.Begin);
+ }
+
+ //s_tracer.WriteLine("blocks seek position = {0}", _stream.Position);
+ }
+ catch (Exception e)
+ {
+ if ((e is IOException) ||
+ (e is ArgumentException) ||
+ (e is System.Security.SecurityException) ||
+ (e is UnauthorizedAccessException) ||
+ (e is ArgumentNullException))
+ {
+ // Exception contains specific message about the error occured and so no need for errordetails.
+ _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, "System.IO.Stream"));
+ }
+ else
+ throw;
+ }
+ }
+ private bool ReadByLine(bool waitChanges, ArrayList blocks, bool readBackward)
+ {
+ // Reading lines as strings
+ string line = readBackward ? _backReader.ReadLine() : _reader.ReadLine();
+
+ if (line != null)
+ blocks.Add(line);
+
+ int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
+ if (peekResult == -1)
+ return false;
+ else
+ return true;
+ }
+
+ private bool ReadDelimited(bool waitChanges, ArrayList blocks, bool readBackward, string actualDelimiter)
+ {
+ if (_isRawStream)
+ {
+ // when -Raw is used we want to anyway read the whole thing
+ // so avoiding the while loop by reading the entire content.
+ string contentRead = _reader.ReadToEnd();
+
+ if (contentRead.Length > 0)
+ {
+ blocks.Add(contentRead);
+ }
+
+ // We already read whole stream so return EOF.
+ return false;
+ }
+
+
+ // Since the delimiter is a string, we're essentially
+ // dealing with a "find the substring" algorithm, but with
+ // the additional restriction that we cannot read past the
+ // end of the delimiter. If we read past the end of the delimiter,
+ // then we'll eat up bytes that we need from the stream.
+ // The solution is a modified Boyer-Moore string search algorithm.
+ // This version retains the sub-linear search performance (via the
+ // lookup tables).
+ int numRead = 0;
+ int currentOffset = actualDelimiter.Length;
+ Span readBuffer = stackalloc char[currentOffset];
+ bool delimiterNotFound = true;
+ _currentLineContent.Clear();
+
+ do
+ {
+ // Read in the required batch of characters
+ numRead = readBackward
+ ? _backReader.Read(readBuffer.Slice(0, currentOffset))
+ : _reader.Read(readBuffer.Slice(0, currentOffset));
+
+ if (numRead > 0)
+ {
+
+ _currentLineContent.Append(readBuffer.Slice(0, numRead));
+
+ // Look up the final character in our offset table.
+ // If the character doesn't exist in the lookup table, then it's not in
+ // our search key. That means the match must happen strictly /after/ the
+ // current position. Because of that, we can feel confident reading in the
+ // number of characters in the search key, without the risk of reading too many.
+ var currentChar = _currentLineContent[_currentLineContent.Length - 1];
+ currentOffset = _offsetDictionary[currentChar];
+ //currentOffset = _offsetDictionary[Unsafe.As(ref currentChar)];
+
+ // We want to keep reading if delimiter not found and we haven't hit the end of stream
+ delimiterNotFound = true;
+
+ // If the final letters matched, then we will get an offset of "0".
+ // In that case, we'll either have a match (and break from the while loop,)
+ // or we need to move the scan forward one position.
+ if (currentOffset == 0)
+ {
+ currentOffset = 1;
+
+ if (actualDelimiter.Length <= _currentLineContent.Length)
+ {
+ delimiterNotFound = false;
+ int i = 0;
+ int j = _currentLineContent.Length - actualDelimiter.Length;
+ for (; i < actualDelimiter.Length; i++, j++)
+ {
+ if (actualDelimiter[i] != _currentLineContent[j])
+ {
+ delimiterNotFound = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ } while (delimiterNotFound && (numRead != 0));
+
+ // We've reached the end of stream or end of line.
+ if (_currentLineContent.Length > 0)
+ {
+ // Add the block read to the ouptut array list, trimming a trailing delimiter, if present.
+ // Note: If -Tail was specified, we get here in the course of 2 distinct passes:
+ // - Once while reading backward simply to determine the appropriate *start position* for later forward reading, ignoring the content of the blocks read (in reverse).
+ // - Then again during forward reading, for regular output processing; it is only then that trimming the delimiter is necessary.
+ // (Trimming it during backward reading would not only be unnecessary, but could interfere with determining the correct start position.)
+ blocks.Add(
+ !readBackward && !delimiterNotFound
+ ? _currentLineContent.ToString(0, _currentLineContent.Length - actualDelimiter.Length)
+ : _currentLineContent.ToString()
+ );
+ }
+
+ int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
+ if (peekResult != -1)
+ return true;
+ else
+ {
+ if (readBackward && _currentLineContent.Length > 0)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ private bool ReadByteEncoded(bool waitChanges, ArrayList blocks, bool readBack)
+ {
+ if (_isRawStream)
+ {
+ // if RawSteam, read all bytes and return. When RawStream is used, we dont
+ // support -first, -last
+ byte[] bytes = new byte[_stream.Length];
+ int numBytesToRead = (int)_stream.Length;
+ int numBytesRead = 0;
+ while (numBytesToRead > 0)
+ {
+ // Read may return anything from 0 to numBytesToRead.
+ int n = _stream.Read(bytes, numBytesRead, numBytesToRead);
+
+ // Break when the end of the stream is reached.
+ if (n == 0)
+ break;
+
+ numBytesRead += n;
+ numBytesToRead -= n;
+ }
+
+ if (numBytesRead == 0)
+ {
+ return false;
+ }
+ else
+ {
+ blocks.Add(bytes);
+ return true;
+ }
+ }
+
+ if (readBack)
+ {
+ if (_stream.Position == 0)
+ {
+ return false;
+ }
+
+ _stream.Position--;
+ blocks.Add((byte)_stream.ReadByte());
+ _stream.Position--;
+ return true;
+ }
+
+ // Reading bytes not strings
+ int byteRead = _stream.ReadByte();
+
+ // Add the byte we read to the list of blocks
+ if (byteRead != -1)
+ {
+ blocks.Add((byte)byteRead);
+ return true;
+ }
+ else
+ return false;
+ }
+ private void CreateStreams(Stream stream, Encoding encoding)
+ {
+ _stream = stream;
+
+
+ if (!_usingByteEncoding)
+ {
+ // Open the reader stream
+ _reader = new StreamReader(_stream, encoding);
+ _backReader = new StreamContentBackReader(_stream, encoding);
+
+ // Open the writer stream
+ if (_reader != null)
+ {
+ _reader.Peek();
+ encoding = _reader.CurrentEncoding;
+ }
+
+ _writer = new StreamWriter(_stream, encoding);
+ }
+ }
+
+ ///
+ /// Moves the current stream position.
+ ///
+ ///
+ /// The offset from the origin to move the position to.
+ ///
+ ///
+ /// The origin from which the offset is calculated.
+ ///
+ public void Seek(long offset, SeekOrigin origin)
+ {
+ if (_writer != null) { _writer.Flush(); }
+
+ _stream.Seek(offset, origin);
+
+ if (_writer != null) { _writer.Flush(); }
+
+ if (_reader != null) { _reader.DiscardBufferedData(); }
+
+ if (_backReader != null) { _backReader.DiscardBufferedData(); }
+ }
+
+ public virtual void FinalizeStream()
+ {
+
+ }
+
+ ///
+ /// Closes the stream.
+ ///
+ public void Close()
+ {
+ bool streamClosed = false;
+
+ if (_writer != null)
+ {
+ try
+ {
+ _writer.Flush();
+ _writer.Dispose();
+ }
+ finally
+ {
+ streamClosed = true;
+ }
+ }
+
+ if (_reader != null)
+ {
+ _reader.Dispose();
+ streamClosed = true;
+ }
+
+ if (_backReader != null)
+ {
+ _backReader.Dispose();
+ streamClosed = true;
+ }
+
+ if (!streamClosed)
+ {
+ _stream.Flush();
+ _stream.Dispose();
+ }
+ }
+
+ ///
+ /// Writes the specified object to the stream.
+ ///
+ ///
+ /// The objects to write to the stream
+ ///
+ ///
+ /// The objects written to the stream.
+ ///
+ public IList Write(IList content)
+ {
+
+ foreach (object line in content)
+ {
+ object[] contentArray = line as object[];
+ if (contentArray != null)
+ {
+ foreach (object obj in contentArray)
+ {
+ WriteObject(obj);
+ }
+ }
+ else
+ {
+ WriteObject(line);
+ }
+ }
+
+ return content;
+ }
+
+ private void WriteObject(object content)
+ {
+ if (content == null)
+ {
+ return;
+ }
+
+ if (_usingByteEncoding)
+ {
+
+ try
+ {
+ byte byteToWrite = (byte)content;
+ _stream.WriteByte(byteToWrite);
+ }
+ catch (InvalidCastException)
+ {
+ throw TraceSource.NewArgumentException("content", Exceptions.ByteEncodingError);
+ }
+ }
+ else
+ {
+ if (_suppressNewline)
+ {
+ _writer.Write(content.ToString());
+ }
+ else
+ {
+ _writer.WriteLine(content.ToString());
+ }
+ }
+ }
+
+ ///
+ /// Closes the stream.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ internal void Dispose(bool isDisposing)
+ {
+ if (isDisposing)
+ {
+ if (_stream != null)
+ _stream.Dispose();
+ if (_reader != null)
+ _reader.Dispose();
+ if (_backReader != null)
+ _backReader.Dispose();
+ if (_writer != null)
+ _writer.Dispose();
+ }
+ }
+ }
+
+ internal sealed class StreamContentBackReader : StreamReader
+ {
+ internal StreamContentBackReader(Stream stream, Encoding encoding)
+ : base(stream, encoding)
+ {
+ _stream = stream;
+ if (_stream.Length > 0)
+ {
+ long curPosition = _stream.Position;
+ _stream.Seek(0, SeekOrigin.Begin);
+ base.Peek();
+ _stream.Position = curPosition;
+ _currentEncoding = base.CurrentEncoding;
+ _currentPosition = _stream.Position;
+
+ // Get the oem encoding and system current ANSI code page
+ _oemEncoding = EncodingConversion.Convert(null, EncodingConversion.OEM);
+ _defaultAnsiEncoding = EncodingConversion.Convert(null, EncodingConversion.Default);
+ }
+ }
+
+ private readonly Stream _stream;
+ private readonly Encoding _currentEncoding;
+ private readonly Encoding _oemEncoding;
+ private readonly Encoding _defaultAnsiEncoding;
+
+ private const int BuffSize = 4096;
+ private readonly byte[] _byteBuff = new byte[BuffSize];
+ private readonly char[] _charBuff = new char[BuffSize];
+ private int _byteCount = 0;
+ private int _charCount = 0;
+ private long _currentPosition = 0;
+ private bool? _singleByteCharSet = null;
+
+ private const byte BothTopBitsSet = 0xC0;
+ private const byte TopBitUnset = 0x80;
+
+ ///
+ /// If the given encoding is OEM or Default, check to see if the code page
+ /// is SBCS(single byte character set).
+ ///
+ ///
+ private bool IsSingleByteCharacterSet()
+ {
+ if (_singleByteCharSet != null)
+ return (bool)_singleByteCharSet;
+
+ // Porting note: only UTF-8 is supported on Linux, which is not an SBCS
+ if ((_currentEncoding.Equals(_oemEncoding) ||
+ _currentEncoding.Equals(_defaultAnsiEncoding))
+ && Platform.IsWindows)
+ {
+ NativeMethods.CPINFO cpInfo;
+ if (NativeMethods.GetCPInfo((uint)_currentEncoding.CodePage, out cpInfo) &&
+ cpInfo.MaxCharSize == 1)
+ {
+ _singleByteCharSet = true;
+ return true;
+ }
+ }
+
+ _singleByteCharSet = false;
+ return false;
+ }
+
+ ///
+ /// We don't support this method because it is not used by the ReadBackward method in StreamContentReaderWriter.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override int ReadBlock(char[] buffer, int index, int count)
+ {
+ // This method is not supposed to be used
+ throw TraceSource.NewNotSupportedException();
+ }
+
+ ///
+ /// We don't support this method because it is not used by the ReadBackward method in StreamContentReaderWriter.
+ ///
+ ///
+ public override string ReadToEnd()
+ {
+ // This method is not supposed to be used
+ throw TraceSource.NewNotSupportedException();
+ }
+
+ ///
+ /// Reset the internal character buffer. Use it only when the position of the internal buffer and
+ /// the base stream do not match. These positions can become mismatch when the user read the data
+ /// into the buffer and then seek a new position in the underlying stream.
+ ///
+ internal new void DiscardBufferedData()
+ {
+ base.DiscardBufferedData();
+ _currentPosition = _stream.Position;
+ _charCount = 0;
+ _byteCount = 0;
+ }
+
+ ///
+ /// Return the current actual stream position.
+ ///
+ ///
+ internal long GetCurrentPosition()
+ {
+ if (_charCount == 0)
+ return _currentPosition;
+
+ // _charCount > 0
+ int byteCount = _currentEncoding.GetByteCount(_charBuff, 0, _charCount);
+ return (_currentPosition + byteCount);
+ }
+
+ ///
+ /// Get the number of bytes the delimiter will
+ /// be encoded to.
+ ///
+ ///
+ ///
+ internal int GetByteCount(string delimiter)
+ {
+ char[] chars = delimiter.ToCharArray();
+ return _currentEncoding.GetByteCount(chars, 0, chars.Length);
+ }
+
+ ///
+ /// Peek the next character.
+ ///
+ /// Return -1 if we reach the head of the stream.
+ public override int Peek()
+ {
+ if (_charCount == 0)
+ {
+ if (RefillCharBuffer() == -1)
+ {
+ return -1;
+ }
+ }
+
+ // Return the next available character, but DONT consume it (don't advance the _charCount)
+ return (int)_charBuff[_charCount - 1];
+ }
+
+ ///
+ /// Read the next character.
+ ///
+ /// Return -1 if we reach the head of the stream.
+ public override int Read()
+ {
+ if (_charCount == 0)
+ {
+ if (RefillCharBuffer() == -1)
+ {
+ return -1;
+ }
+ }
+
+ _charCount--;
+ return _charBuff[_charCount];
+ }
+
+ ///
+ /// Read a specific maximum of characters from the current stream into a buffer.
+ ///
+ /// Output buffer.
+ /// Start position to write with.
+ /// Number of bytes to read.
+ /// Return the number of characters read, or -1 if we reach the head of the stream.
+ /// Return the number of characters read, or -1 if we reach the head of the stream.
+ public override int Read(char[] buffer, int index, int count)
+ {
+ return ReadSpan(new Span(buffer, index, count));
+ }
+
+ ///
+ /// Read characters from the current stream into a Span buffer.
+ ///
+ /// Output buffer.
+ /// Return the number of characters read, or -1 if we reach the head of the stream.
+ public override int Read(Span buffer)
+ {
+ return ReadSpan(buffer);
+ }
+
+ private int ReadSpan(Span buffer)
+ {
+ // deal with the argument validation
+ int charRead = 0;
+ int index = 0;
+ int count = buffer.Length;
+
+ do
+ {
+ if (_charCount == 0)
+ {
+ if (RefillCharBuffer() == -1)
+ {
+ return charRead;
+ }
+ }
+
+ int toRead = _charCount > count ? count : _charCount;
+
+ for (; toRead > 0; toRead--, count--, charRead++)
+ {
+ buffer[index++] = _charBuff[--_charCount];
+ }
+ }
+ while (count > 0);
+
+ return charRead;
+ }
+
+ ///
+ /// Read a line from the current stream.
+ ///
+ /// Return null if we reach the head of the stream.
+ public override string ReadLine()
+ {
+ if (_charCount == 0 && RefillCharBuffer() == -1)
+ {
+ return null;
+ }
+
+ int charsToRemove = 0;
+ StringBuilder line = new StringBuilder();
+
+ if (_charBuff[_charCount - 1] == '\r' ||
+ _charBuff[_charCount - 1] == '\n')
+ {
+ charsToRemove++;
+ line.Insert(0, _charBuff[--_charCount]);
+
+ if (_charBuff[_charCount] == '\n')
+ {
+ if (_charCount == 0 && RefillCharBuffer() == -1)
+ {
+ return string.Empty;
+ }
+
+ if (_charCount > 0 && _charBuff[_charCount - 1] == '\r')
+ {
+ charsToRemove++;
+ line.Insert(0, _charBuff[--_charCount]);
+ }
+ }
+ }
+
+ do
+ {
+ while (_charCount > 0)
+ {
+ if (_charBuff[_charCount - 1] == '\r' ||
+ _charBuff[_charCount - 1] == '\n')
+ {
+ line.Remove(line.Length - charsToRemove, charsToRemove);
+ return line.ToString();
+ }
+ else
+ {
+ line.Insert(0, _charBuff[--_charCount]);
+ }
+ }
+
+ if (RefillCharBuffer() == -1)
+ {
+ line.Remove(line.Length - charsToRemove, charsToRemove);
+ return line.ToString();
+ }
+ } while (true);
+ }
+
+ ///
+ /// Refill the internal character buffer.
+ ///
+ ///
+ private int RefillCharBuffer()
+ {
+ if ((RefillByteBuff()) == -1)
+ {
+ return -1;
+ }
+
+ _charCount = _currentEncoding.GetChars(_byteBuff, 0, _byteCount, _charBuff, 0);
+ return _charCount;
+ }
+
+ ///
+ /// Refill the internal byte buffer.
+ ///
+ ///
+ private int RefillByteBuff()
+ {
+ long lengthLeft = _stream.Position;
+
+ if (lengthLeft == 0)
+ {
+ return -1;
+ }
+
+ int toRead = lengthLeft > BuffSize ? BuffSize : (int)lengthLeft;
+ _stream.Seek(-toRead, SeekOrigin.Current);
+
+ if (_currentEncoding.Equals(Encoding.UTF8))
+ {
+ // It's UTF-8, we need to detect the starting byte of a character
+ do
+ {
+ _currentPosition = _stream.Position;
+ byte curByte = (byte)_stream.ReadByte();
+ if ((curByte & BothTopBitsSet) == BothTopBitsSet ||
+ (curByte & TopBitUnset) == 0x00)
+ {
+ _byteBuff[0] = curByte;
+ _byteCount = 1;
+ break;
+ }
+ } while (lengthLeft > _stream.Position);
+
+ if (lengthLeft == _stream.Position)
+ {
+ // Cannot find a starting byte. The stream is NOT UTF-8 format. Read 'toRead' number of bytes
+ _stream.Seek(-toRead, SeekOrigin.Current);
+ _byteCount = 0;
+ }
+
+ _byteCount += _stream.Read(_byteBuff, _byteCount, (int)(lengthLeft - _stream.Position));
+ _stream.Position = _currentPosition;
+ }
+ else if (_currentEncoding.Equals(Encoding.Unicode) ||
+ _currentEncoding.Equals(Encoding.BigEndianUnicode) ||
+ _currentEncoding.Equals(Encoding.UTF32) ||
+ _currentEncoding.Equals(Encoding.ASCII) ||
+ IsSingleByteCharacterSet())
+ {
+ // Unicode -- two bytes per character
+ // BigEndianUnicode -- two types per character
+ // UTF-32 -- four bytes per character
+ // ASCII -- one byte per character
+ // The BufferSize will be a multiple of 4, so we can just read toRead number of bytes
+ // if the current stream is encoded by any of these formatting
+
+ // If IsSingleByteCharacterSet() returns true, we are sure that the given encoding is OEM
+ // or Default, and it is SBCS(single byte character set) code page -- one byte per character
+ _currentPosition = _stream.Position;
+ _byteCount = _stream.Read(_byteBuff, 0, toRead);
+ _stream.Position = _currentPosition;
+ }
+ else
+ {
+ // OEM and ANSI code pages include multibyte CJK code pages. If the current code page
+ // is MBCS(multibyte character set), we cannot detect a starting byte.
+ // UTF-7 has some characters encoded into UTF-16 and then in Modified Base64,
+ // the start of these characters is indicated by a '+' sign, and the end is
+ // indicated by a character that is not in Modified Base64 set.
+ // For these encodings, we cannot detect a starting byte with confidence when
+ // reading bytes backward. Throw out exception in these cases.
+ string errMsg = String.Format(
+ Exceptions.ReadBackward_Encoding_NotSupport,
+ _currentEncoding.EncodingName);
+ throw new BackReaderEncodingNotSupportedException(errMsg, _currentEncoding.EncodingName);
+ }
+
+ return _byteCount;
+ }
+ private static class NativeMethods
+ {
+ // Default values
+ private const int MAX_DEFAULTCHAR = 2;
+ private const int MAX_LEADBYTES = 12;
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct CPINFO
+ {
+ [MarshalAs(UnmanagedType.U4)]
+ internal int MaxCharSize;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_DEFAULTCHAR)]
+ public byte[] DefaultChar;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_LEADBYTES)]
+ public byte[] LeadBytes;
+ };
+
+ ///
+ /// Get information on a named code page.
+ ///
+ ///
+ ///
+ ///
+ [DllImport(PinvokeDllNames.GetCPInfoDllName, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetCPInfo(uint codePage, out CPINFO lpCpInfo);
+ }
+
+ ///
+ /// The exception that indicates the encoding is not supported when reading backward.
+ ///
+ internal sealed class BackReaderEncodingNotSupportedException : NotSupportedException
+ {
+ internal BackReaderEncodingNotSupportedException(string message, string encodingName)
+ : base(message)
+ {
+ EncodingName = encodingName;
+ }
+
+ internal BackReaderEncodingNotSupportedException(string encodingName)
+ {
+ EncodingName = encodingName;
+ }
+
+ ///
+ /// Get the encoding name.
+ ///
+ internal string EncodingName { get; }
+ }
+ }
+
+}
+
diff --git a/src/Microsoft.PowerShell.Archive/utils/StreamContentDynamicParameters.cs b/src/Microsoft.PowerShell.Archive/utils/StreamContentDynamicParameters.cs
new file mode 100644
index 0000000..2fac3b5
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/StreamContentDynamicParameters.cs
@@ -0,0 +1,180 @@
+using System;
+
+using System.Collections;
+using System.Collections.Generic;
+
+using System.Text;
+using System.Management.Automation;
+using System.Management.Automation.Provider;
+using System.Management.Automation.Runspaces;
+using System.Management.Automation.Internal;
+using Microsoft.PowerShell.Commands;
+
+namespace Microsoft.PowerShell.Archive
+{
+
+ ///
+ /// Defines the dynamic parameters used by both the content reader and writer.
+ ///
+ public class StreamContentDynamicParameterBase : FileSystemContentDynamicParametersBase
+ {
+ ///
+ /// Gets or sets the encoding method used when
+ /// reading data from the file.
+ ///
+ [Parameter]
+ [ArgumentToEncodingTransformationAttribute()]
+ [ArgumentEncodingCompletionsAttribute]
+ [ValidateNotNullOrEmpty]
+ public Encoding Encoding
+ {
+ get
+ {
+ return _encoding;
+ }
+
+ set
+ {
+ _encoding = value;
+ // If an encoding was explicitly set, be sure to capture that.
+ WasStreamTypeSpecified = true;
+ }
+ }
+
+ //private Encoding _encoding = ClrFacade.GetDefaultEncoding();
+ private Encoding _encoding = Encoding.Default;
+
+ ///
+ /// Return file contents as a byte stream or create file from a series of bytes.
+ ///
+ [Parameter]
+ public SwitchParameter AsByteStream { get; set; }
+
+#if !UNIX
+ ///
+ /// A parameter to return a stream of an item.
+ ///
+ [Parameter]
+ public string Stream { get; set; }
+#endif
+
+ ///
+ /// Gets the status of the StreamType parameter. Returns true
+ /// if the stream was opened with a user-specified encoding, false otherwise.
+ ///
+ public bool WasStreamTypeSpecified { get; private set; }
+
+ }
+
+ ///
+ /// Defines the dynamic parameters used by the set-content and
+ /// add-content cmdlets.
+ ///
+
+ public class StreamContentWriterDynamicParameters : StreamContentDynamicParameterBase
+ {
+ ///
+ /// False to add a newline to the end of the output string, true if not.
+ ///
+ [Parameter]
+ public SwitchParameter NoNewline
+ {
+ get
+ {
+ return _suppressNewline;
+ }
+
+ set
+ {
+ _suppressNewline = value;
+ }
+ }
+
+ private bool _suppressNewline = false;
+ }
+
+ ///
+ /// Defines the dynamic parameters used by the get-content cmdlet.
+ ///
+ public class StreamContentReaderDynamicParameters : StreamContentDynamicParameterBase
+ {
+ ///
+ /// Gets or sets the delimiter to use when reading the file. Custom delimiters
+ /// may not be used when the file is opened with a "Byte" encoding.
+ ///
+ [Parameter]
+ public string Delimiter
+ {
+ get
+ {
+ return _delimiter;
+ }
+
+ set
+ {
+ DelimiterSpecified = true;
+ _delimiter = value;
+ }
+ }
+
+ private string _delimiter = "\n";
+
+ ///
+ /// When the Raw switch is present, we don't do any breaks on newlines,
+ /// and only emit one object to the pipeline: all of the content.
+ ///
+ [Parameter]
+ public SwitchParameter Raw
+ {
+ get
+ {
+ return _isRaw;
+ }
+
+ set
+ {
+ _isRaw = value;
+ }
+ }
+
+ private bool _isRaw;
+
+ ///
+ /// Gets the status of the delimiter parameter. Returns true
+ /// if the delimiter was explicitly specified by the user, false otherwise.
+ ///
+ public bool DelimiterSpecified
+ {
+ get; private set;
+ // get
+ }
+
+ ///
+ /// The number of content items to retrieve from the back of the file.
+ ///
+ [Parameter(ValueFromPipelineByPropertyName = true)]
+ // [Alias("Last")]
+ public int Trail
+ {
+ set
+ {
+ _backCount = value;
+ _tailSpecified = true;
+ }
+
+ get { return _backCount; }
+ }
+
+ private int _backCount = -1;
+ private bool _tailSpecified = false;
+
+ }
+
+ ///
+ /// Defines the dynamic parameters used by the Clear-Content cmdlet.
+ ///
+ public class StreamContentClearContentDynamicParameters
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.Archive/utils/TraceSource.cs b/src/Microsoft.PowerShell.Archive/utils/TraceSource.cs
new file mode 100644
index 0000000..4295a45
--- /dev/null
+++ b/src/Microsoft.PowerShell.Archive/utils/TraceSource.cs
@@ -0,0 +1,220 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using System.Management.Automation.Internal;
+using System.Management.Automation;
+
+namespace Microsoft.PowerShell.Archive
+{
+ ///
+ /// A TraceSource is a representation of a System.Diagnostics.TraceSource instance
+ /// that is used the the Monad components to produce trace output.
+ ///
+ ///
+ /// It is permitted to subclass
+ /// but there is no established scenario for doing this, nor has it been tested.
+ ///
+
+ internal partial class TraceSource
+ {
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This is not allowed to call other
+ /// Throw*Exception variants, since they call this.
+ ///
+ /// Exception instance ready to throw.
+ internal static PSNotSupportedException NewNotSupportedException()
+ {
+ string message = String.Format(Exceptions.NotSupported,
+ new System.Diagnostics.StackTrace().GetFrame(0).ToString());
+ var e = new PSNotSupportedException(message);
+
+ return e;
+ }
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This is not allowed to call other
+ /// Throw*Exception variants, since they call this.
+ ///
+ ///
+ /// The name of the parameter whose argument value was null
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentNullException NewArgumentNullException(string paramName)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw new ArgumentNullException("paramName");
+ }
+
+ string message = String.Format(Exceptions.ArgumentNull, paramName);
+ var e = new PSArgumentNullException(paramName, message);
+
+ return e;
+ }
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This variant allows the caller to
+ /// specify alternate template text, but only in assembly S.M.A.Core.
+ ///
+ ///
+ /// The name of the parameter whose argument value was invalid
+ ///
+ ///
+ /// The template string for this error
+ ///
+ ///
+ /// Objects corresponding to {0}, {1}, etc. in the resource string
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentNullException NewArgumentNullException(
+ string paramName, string resourceString, params object[] args)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw NewArgumentNullException("paramName");
+ }
+
+ if (string.IsNullOrEmpty(resourceString))
+ {
+ throw NewArgumentNullException("resourceString");
+ }
+
+ string message = String.Format(resourceString, args);
+
+ // Note that the paramName param comes first
+ var e = new PSArgumentNullException(paramName, message);
+
+ return e;
+ }
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This variant uses the default
+ /// ArgumentException template text. This is not allowed to call
+ /// other Throw*Exception variants, since they call this.
+ ///
+ ///
+ /// The name of the parameter whose argument value was invalid
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentException NewArgumentException(string paramName)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw new ArgumentNullException("paramName");
+ }
+
+ string message = String.Format(Exceptions.Argument, paramName);
+ // Note that the message param comes first
+ var e = new PSArgumentException(message, paramName);
+
+ return e;
+ }
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This variant allows the caller to
+ /// specify alternate template text, but only in assembly S.M.A.Core.
+ ///
+ ///
+ /// The name of the parameter whose argument value was invalid
+ ///
+ ///
+ /// The template string for this error
+ ///
+ ///
+ /// Objects corresponding to {0}, {1}, etc. in the resource string
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentException NewArgumentException(
+ string paramName, string resourceString, params object[] args)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw NewArgumentNullException("paramName");
+ }
+
+ if (string.IsNullOrEmpty(resourceString))
+ {
+ throw NewArgumentNullException("resourceString");
+ }
+
+ string message = String.Format(resourceString, args);
+
+ // Note that the message param comes first
+ var e = new PSArgumentException(message, paramName);
+
+ return e;
+ }
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This variant uses the default
+ /// ArgumentOutOfRangeException template text. This is not allowed to call
+ /// other Throw*Exception variants, since they call this.
+ ///
+ ///
+ /// The name of the parameter whose argument value was out of range
+ ///
+ ///
+ /// The value of the argument causing the exception
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentOutOfRangeException NewArgumentOutOfRangeException(string paramName, object actualValue)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw new ArgumentNullException("paramName");
+ }
+
+ string message = String.Format(Exceptions.ArgumentOutOfRange, paramName);
+ var e = new PSArgumentOutOfRangeException(paramName, actualValue, message);
+
+ return e;
+ }
+
+ ///
+ /// Traces the Message and StackTrace properties of the exception
+ /// and returns the new exception. This variant allows the caller to
+ /// specify alternate template text, but only in assembly S.M.A.Core.
+ ///
+ ///
+ /// The name of the parameter whose argument value was invalid
+ ///
+ ///
+ /// The value of the argument causing the exception
+ ///
+ ///
+ /// The template string for this error
+ ///
+ ///
+ /// Objects corresponding to {0}, {1}, etc. in the resource string
+ ///
+ /// Exception instance ready to throw.
+ internal static PSArgumentOutOfRangeException NewArgumentOutOfRangeException(
+ string paramName, object actualValue, string resourceString, params object[] args)
+ {
+ if (string.IsNullOrEmpty(paramName))
+ {
+ throw NewArgumentNullException("paramName");
+ }
+
+ if (string.IsNullOrEmpty(resourceString))
+ {
+ throw NewArgumentNullException("resourceString");
+ }
+
+ string message = String.Format(resourceString, args);
+ var e = new PSArgumentOutOfRangeException(paramName, actualValue, message);
+
+ return e;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/ResGen/Program.cs b/src/ResGen/Program.cs
new file mode 100644
index 0000000..44ae9a7
--- /dev/null
+++ b/src/ResGen/Program.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml.Linq;
+
+namespace ConsoleApplication
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ IEnumerable dirs;
+ string fileFilter;
+
+ if (args.Length == 1)
+ {
+ // We are assuming resgen is run with 'dotnet run pathToResxFile.resx'.
+ fileFilter = Path.GetFileName(args[0]);
+ string moduleDirectory = Path.GetDirectoryName(Path.GetDirectoryName(args[0]));
+ dirs = new List() { moduleDirectory };
+ }
+ else
+ {
+ // We are assuming resgen is run with 'dotnet run'
+ // so we can use relative path to get a parent directory
+ // to process all *.resx files in all project subdirectories.
+ fileFilter = "*.resx";
+ dirs = Directory.EnumerateDirectories("..");
+ }
+
+ foreach (string folder in dirs)
+ {
+ string moduleName = Path.GetFileName(folder);
+ string resourcePath = Path.Combine(folder, "resources");
+
+ if (Directory.Exists(resourcePath))
+ {
+ string genFolder = Path.Combine(folder, "gen");
+ if (!Directory.Exists(genFolder))
+ {
+ Directory.CreateDirectory(genFolder);
+ }
+
+ foreach (string resxPath in Directory.EnumerateFiles(resourcePath, fileFilter))
+ {
+ string className = Path.GetFileNameWithoutExtension(resxPath);
+ string sourceCode = GetStronglyTypeCsFileForResx(resxPath, moduleName, className);
+ string outPath = Path.Combine(genFolder, className + ".cs");
+ Console.WriteLine("ResGen for " + outPath);
+ File.WriteAllText(outPath, sourceCode);
+ }
+ }
+ }
+ }
+
+ private static string GetStronglyTypeCsFileForResx(string xmlPath, string moduleName, string className)
+ {
+ // Example
+ //
+ // className = Full.Name.Of.The.ClassFoo
+ // shortClassName = ClassFoo
+ // namespaceName = Full.Name.Of.The
+
+ string shortClassName = className;
+ string namespaceName = null;
+ int lastIndexOfDot = className.LastIndexOf('.');
+ if (lastIndexOfDot != -1)
+ {
+ namespaceName = className.Substring(0, lastIndexOfDot);
+ shortClassName = className.Substring(lastIndexOfDot + 1);
+ }
+
+ var entries = new StringBuilder();
+ XElement root = XElement.Parse(File.ReadAllText(xmlPath));
+ foreach (var data in root.Elements("data"))
+ {
+ string value = data.Value.Replace("\n", "\n ///");
+ string name = data.Attribute("name").Value.Replace(' ', '_');
+ entries.AppendFormat(ENTRY, name, value);
+ }
+
+ string bodyCode = string.Format(BODY, shortClassName, moduleName, entries.ToString(), className);
+ if (namespaceName != null)
+ {
+ bodyCode = string.Format(NAMESPACE, namespaceName, bodyCode);
+ }
+
+ string resultCode = string.Format(BANNER, bodyCode).Replace("\r\n?|\n", "\r\n");
+ return resultCode;
+ }
+
+ private static readonly string BANNER = @"
+//------------------------------------------------------------------------------
+//
+// This code was generated by a dotnet run from src\ResGen folder.
+// To add or remove a member, edit your .resx file then rerun src\ResGen.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+{0}
+";
+
+ private static readonly string NAMESPACE = @"
+namespace {0} {{
+{1}
+}}
+";
+ private static readonly string BODY = @"
+using System;
+using System.Reflection;
+
+///
+/// A strongly-typed resource class, for looking up localized strings, etc.
+///
+[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""System.Resources.Tools.StronglyTypedResourceBuilder"", ""4.0.0.0"")]
+[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+
+internal class {0} {{
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(""Microsoft.Performance"", ""CA1811:AvoidUncalledPrivateCode"")]
+ internal {0}() {{
+ }}
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {{
+ get {{
+ if (object.ReferenceEquals(resourceMan, null)) {{
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager(""{1}.resources.{3}"", typeof({0}).Assembly);
+ resourceMan = temp;
+ }}
+
+ return resourceMan;
+ }}
+ }}
+
+ ///
+ /// Overrides the current threads CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {{
+ get {{
+ return resourceCulture;
+ }}
+
+ set {{
+ resourceCulture = value;
+ }}
+ }}
+ {2}
+}}
+";
+
+ private static readonly string ENTRY = @"
+
+ ///
+ /// Looks up a localized string similar to {1}
+ ///
+ internal static string {0} {{
+ get {{
+ return ResourceManager.GetString(""{0}"", resourceCulture);
+ }}
+ }}
+";
+
+ }
+}
\ No newline at end of file
diff --git a/src/ResGen/ResGen.csproj b/src/ResGen/ResGen.csproj
new file mode 100644
index 0000000..aa10d73
--- /dev/null
+++ b/src/ResGen/ResGen.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Generates C# typed bindings for .resx files
+ netcoreapp3.0
+ resgen
+ Exe
+ true
+ true
+ win7-x86;win7-x64;osx-x64;linux-x64
+
+
+
\ No newline at end of file