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