├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── ChangeLog.txt ├── PS1C.sln ├── README.md ├── Tests ├── PS1C │ ├── Add-Content.Tests.ps1 │ ├── Clear-Content.Tests.ps1 │ ├── Get-Content.Tests.ps1 │ ├── New-Item.Tests.ps1 │ ├── Remove-Item.Tests.ps1 │ ├── Rename-Item.Tests.ps1 │ └── Set-Content.Tests.ps1 └── Test.ps1 ├── docs ├── Development │ └── GettingStarted.md └── cmdlets │ ├── Add-Content.md │ ├── Clear-Content.md │ ├── Get-Content.md │ ├── New-Item.md │ ├── Remove-Item.md │ ├── Rename-Item.md │ └── Set-Content.md └── src ├── ResGen ├── Program.cs └── ResGen.csproj └── ps1c ├── ArchiveContentStream.cs ├── ArchiveItemInfo.cs ├── ArchiveItemStream.cs ├── ArchivePSDriveInfo.cs ├── ArchiveProvider.cs ├── PS1c.csproj ├── resources ├── ArchiveProviderStrings.resx ├── Exceptions.resx ├── PS1C.ArchiveFileInfo.Format.ps1xml ├── PS1C.psd1 └── PS1C.psm1 └── utils ├── Assert.cs ├── CorePsPlatform.cs ├── CrossCompilerExtensions.cs ├── EncodingUtils.cs ├── ExtensibleCompletion.cs ├── PinvokeDllNames.cs ├── StreamContent.cs ├── StreamContentDynamicParameters.cs └── TraceSource.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/PublishToGallery.ps1 2 | asdf.ps1 3 | Issue Request.md 4 | Source/PS1C/bin 5 | Source/PS1C/obj 6 | Module/ 7 | 8 | 9 | src/PS1C/bin 10 | src/PS1C/obj 11 | src/PS1C/packages 12 | src/PS1C/gen 13 | src/PS1C/obj 14 | src/RESGEN 15 | src/ResGen/bin/* 16 | src/ResGen/obj/* 17 | src/ResGen/packages 18 | 19 | /PS1C 20 | Tests/PS1C/Zipfile.zip 21 | Scratch/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/bin/Debug//.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}", 15 | "console": "internalConsole", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart" 18 | }, 19 | { 20 | "name": ".NET Core Launch (web)", 21 | "type": "coreclr", 22 | "request": "launch", 23 | "preLaunchTask": "build", 24 | "program": "${workspaceFolder}/bin/Debug//.dll", 25 | "args": [], 26 | "cwd": "${workspaceFolder}", 27 | "stopAtEntry": false, 28 | "internalConsoleOptions": "openOnSessionStart", 29 | "launchBrowser": { 30 | "enabled": true, 31 | "args": "${auto-detect-url}", 32 | "windows": { 33 | "command": "cmd.exe", 34 | "args": "/C start ${auto-detect-url}" 35 | }, 36 | "osx": { 37 | "command": "open" 38 | }, 39 | "linux": { 40 | "command": "xdg-open" 41 | } 42 | }, 43 | "env": { 44 | "ASPNETCORE_ENVIRONMENT": "Development" 45 | }, 46 | "sourceFileMap": { 47 | "/Views": "${workspaceFolder}/Views" 48 | } 49 | }, 50 | { 51 | "name": ".NET Core Attach", 52 | "type": "coreclr", 53 | "request": "attach", 54 | "processId": "${command:pickProcess}" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": "dotnet build", 10 | 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | }, 16 | { 17 | "label": "Run Tests", 18 | "type": "shell", 19 | "command": "pwsh.exe", 20 | "args": [ 21 | "-Command '. ${workspaceFolder}\\Tests\\Test.ps1'" 22 | ], 23 | "dependsOn": "Build", 24 | "group": { 25 | "kind": "build", 26 | "isDefault": true 27 | } 28 | }, 29 | { 30 | "label": "Launch File", 31 | "type": "shell", 32 | "command": "C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", 33 | "args": [ 34 | "-Command '. ${file}\" '" 35 | ], 36 | "group": { 37 | "kind": "build", 38 | "isDefault": true 39 | } 40 | }, 41 | { 42 | "label": "Echo Space", 43 | "type": "shell", 44 | "command": "echo ${workspaceFolder}", 45 | "group": { 46 | "kind": "build", 47 | "isDefault": true 48 | } 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | Version 0.0.90.0 2 | Revamped the repository, and core features. 3 | UTF7 was removed as a supported encoding format. 4 | 5 | Version 0.0.1.1 Added Commands and Invoke-Item 6 | Nothing major just minor tweaks. 7 | New Features 8 | Get-Content -raw 9 | Added the ability to get the raw value of the files inside powershell. 10 | Invoke-Item 11 | Now Invokes any script inside the zip file. 12 | Note: No error handling. 13 | 14 | Version 0.0.1 The First Update 15 | No real feature changes, however I am laying the groundwork to Modify and Edit Files within the ZipFile. s 16 | 17 | Now with less IOPS. 18 | Rewrote code to cache data, and store variables for each drive. 19 | 20 | Cleaned up Default PSObject Output. 21 | Looks a lot more like the file system. 22 | 23 | 24 | 25 | Version 0.0.0 Initial Release 26 | PS1C is a simple Zipfile Reader 27 | 28 | Working Features: 29 | dir 30 | Reads Directory Structure 31 | Get-Content 32 | Reads content of files 33 | 34 | TODO: 35 | Add Write Capability 36 | and Better Stream manipulation. 37 | 38 | 39 | 40 | Example Usage: 41 | 42 | import-module "path\PS1C.dll" 43 | new-psdrive -name APPX -psprovider PS1C -root "$($pwd.path)\ZipFile.zip" 44 | cd APPX: 45 | ls | ft Name, FullName, *dir* 46 | 47 | get-content APPX:\file.txt -------------------------------------------------------------------------------- /PS1C.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.10 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PS1C", "SRC\PS1C\PS1C.csproj", "{B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DCDF473-9667-4F84-B1A3-D3843766DAE8}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B37E6EC2-E22D-407B-AB2D-F4C02C7EDA71}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ExtensibilityGlobals) = postSolution 25 | SolutionGuid = {BB182DC0-D6FC-4C77-807F-2F4618457F7C} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to PowerShell Compressed (PS1C) is a ZipFile Powershell Provider 2 | 3 | ## What is it? 4 | PS1C is a PowerShell Provider that lets you mount zip/archive files as if they were native PSDrive. 5 | 6 | ## Requirements 7 | PS1C works only in PowerShell 6 and up. 8 | 9 | # Getting Started 10 | 11 | ## Installing PS1C 12 | ``` ps1 13 | Install-Module -Name PS1C -Force 14 | ``` 15 | 16 | ## Getting Started 17 | ``` ps1 18 | 19 | Import-Module PS1C -Force 20 | $ZipFile = "$PSScriptRoot\MyZipFile.zip" 21 | New-PSDrive -Name MyZip -PSProvider ZipFile -Root $ZipFile 22 | cd MyZip:\ 23 | ``` 24 | 25 | ## Currently the commandsare supported 26 | 27 | * Add-Content 28 | * Clear-Content 29 | * Get-Content 30 | * New-Item 31 | * Remove-Item 32 | * Rename-Item 33 | * Set-Content 34 | 35 | ## Special Considerations 36 | This is designed to feel as natural to PowerShell's default FileSystemprovider, however not all functionality can be implemented due to actual limitations within the PowerShell Language. 37 | 38 | UTF7 was removed as a supported encoding format. -------------------------------------------------------------------------------- /Tests/PS1C/Add-Content.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Add-Content cmdlet tests" -Tags "CI" { 5 | BeforeAll { 6 | function New-ZipFile { 7 | [CmdletBinding()] 8 | param( 9 | [Parameter(Mandatory)] 10 | [System.String] $Path 11 | ) 12 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 13 | [System.IO.File]::WriteAllBytes($Path, $bytes) 14 | } 15 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 16 | 17 | $zipfilePath = "$env:TEMP\ZipFile.zip" 18 | New-ZipFile -Path $zipfilePath 19 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 20 | 21 | #Current tests run persistanence for zipfile. 22 | Remove-Item TestDrive:\file* -ErrorAction Continue 23 | Remove-Item TestDrive:\dynamic* -ErrorAction Continue 24 | 25 | $file1 = "file1.txt" 26 | New-Item -Path "TestDrive:\$file1" -ItemType File -Force 27 | } 28 | 29 | Context "Add-Content should actually add content" { 30 | It "should Add-Content to TestDrive:\$file1" { 31 | $result = Add-Content -Path TestDrive:\$file1 -Value "ExpectedContent" -PassThru 32 | $result | Should -BeExactly "ExpectedContent" 33 | } 34 | 35 | It "should return expected string from TestDrive:\$file1" { 36 | $result = Get-Content -Path TestDrive:\$file1 37 | $result | Should -BeExactly "ExpectedContent" 38 | } 39 | 40 | It "should Add-Content to TestDrive:\dynamicfile.txt with dynamic parameters" -Pending:($IsLinux -Or $IsMacOS) {#https://github.com/PowerShell/PowerShell/issues/891 41 | $result = Add-Content -Path TestDrive:\dynamicfile.txt -Value "ExpectedContent" -PassThru 42 | $result | Should -BeExactly "ExpectedContent" 43 | } 44 | 45 | It "should return expected string from TestDrive:\dynamicfile.txt" -Pending:($IsLinux -Or $IsMacOS) {#https://github.com/PowerShell/PowerShell/issues/891 46 | $result = Get-Content -Path TestDrive:\dynamicfile.txt 47 | $result | Should -BeExactly "ExpectedContent" 48 | } 49 | 50 | It "should Add-Content to TestDrive:\$file1 even when -Value is `$null" { 51 | $AsItWas = Get-Content -Path TestDrive:\$file1 52 | { Add-Content -Path TestDrive:\$file1 -Value $null -ErrorAction Stop } | Should -Not -Throw 53 | Get-Content -Path TestDrive:\$file1 | Should -BeExactly $AsItWas 54 | } 55 | 56 | It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$null" { 57 | { Add-Content -Path $null -Value "ShouldNotWorkBecausePathIsNull" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.AddContentCommand" 58 | } 59 | 60 | #[BugId(BugDatabase.WindowsOutOfBandReleases, 903880)] 61 | It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$()" { 62 | { Add-Content -Path $() -Value "ShouldNotWorkBecausePathIsInvalid" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.AddContentCommand" 63 | } 64 | 65 | It "Should throw an error on a directory" { 66 | { Add-Content -Path . -Value "WriteContainerContentException" -ErrorAction Stop } | Should -Throw -ErrorId "WriteContainerContentException,Microsoft.PowerShell.Commands.AddContentCommand" 67 | } 68 | 69 | #[BugId(BugDatabase.WindowsOutOfBandReleases, 9058182)] 70 | 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 71 | "hello","world" | Add-Content -Path TestDrive:\dynamicfile2.txt 72 | $result = Get-Content -Path TestDrive:\dynamicfile2.txt 73 | $result.length | Should -Be 2 74 | $result[0] | Should -BeExactly "hello" 75 | $result[1] | Should -BeExactly "world" 76 | } 77 | 78 | It "Should not block reads while writing" { 79 | $logpath = Join-Path $testdrive "test.log" 80 | 81 | Set-Content $logpath -Value "hello" 82 | 83 | $f = [System.IO.FileStream]::new($logpath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) 84 | 85 | Add-Content $logpath -Value "world" 86 | 87 | $f.Close() 88 | 89 | $content = Get-Content $logpath 90 | $content | Should -HaveCount 2 91 | $content[0] | Should -BeExactly "hello" 92 | $content[1] | Should -BeExactly "world" 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Tests/PS1C/Clear-Content.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | # get a random string of characters a-z and A-Z 5 | function Get-RandomString 6 | { 7 | param ( [int]$Length = 8 ) 8 | $chars = .{ ([int][char]'a')..([int][char]'z');([int][char]'A')..([int][char]'Z') } 9 | ([char[]]($chars | Get-Random -Count $Length)) -join "" 10 | } 11 | 12 | # get a random string which is not the name of an existing provider 13 | function Get-NonExistantProviderName 14 | { 15 | param ( [int]$Length = 8 ) 16 | do { 17 | $providerName = Get-RandomString -Length $Length 18 | } until ( $null -eq (Get-PSProvider -PSProvider $providername -ErrorAction SilentlyContinue) ) 19 | $providerName 20 | } 21 | 22 | # get a random string which is not the name of an existing drive 23 | function Get-NonExistantDriveName 24 | { 25 | param ( [int]$Length = 8 ) 26 | do { 27 | $driveName = Get-RandomString -Length $Length 28 | } until ( $null -eq (Get-PSDrive $driveName -ErrorAction SilentlyContinue) ) 29 | $drivename 30 | } 31 | 32 | # get a random string which is not the name of an existing function 33 | function Get-NonExistantFunctionName 34 | { 35 | param ( [int]$Length = 8 ) 36 | do { 37 | $functionName = Get-RandomString -Length $Length 38 | } until ( (Test-Path -Path function:$functionName) -eq $false ) 39 | $functionName 40 | } 41 | 42 | Describe "Clear-Content cmdlet tests" -Tags "CI" { 43 | BeforeAll { 44 | function New-ZipFile { 45 | [CmdletBinding()] 46 | param( 47 | [Parameter(Mandatory)] 48 | [System.String] $Path 49 | ) 50 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 51 | [System.IO.File]::WriteAllBytes($Path, $bytes) 52 | } 53 | 54 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 55 | 56 | $zipfilePath = "$env:TEMP\ZipFile.zip" 57 | New-ZipFile -Path $zipfilePath 58 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 59 | 60 | $file1 = "file1.txt" 61 | $file2 = "file2.txt" 62 | $file3 = "file3.txt" 63 | $content1 = "This is content" 64 | $content2 = "This is content for alternate stream tests" 65 | 66 | 67 | New-Item -Path "TestDrive:\$file1" -ItemType File -Force 68 | New-Item -Path "TestDrive:\$file2" -ItemType File -Value $content1 -Force 69 | New-Item -Path "TestDrive:\$file3" -ItemType File -Value $content2 -Force 70 | 71 | $streamContent = "content for alternate stream" 72 | $streamName = "altStream1" 73 | } 74 | 75 | Context "Clear-Content should actually clear content" { 76 | It "should clear-Content of TestDrive:\$file1" { 77 | Set-Content -Path TestDrive:\$file1 -Value "ExpectedContent" -PassThru | Should -BeExactly "ExpectedContent" 78 | Clear-Content -Path TestDrive:\$file1 79 | } 80 | 81 | It "shouldn't get any content from TestDrive:\$file1" { 82 | $result = Get-Content -Path TestDrive:\$file1 83 | $result | Should -BeNullOrEmpty 84 | } 85 | 86 | # we could suppress the WhatIf output here if we use the testhost, but it's not necessary 87 | It "The filesystem provider supports should process" -skip:(!$IsWindows) { 88 | Clear-Content -Path TestDrive:\$file2 -WhatIf 89 | Get-Content -Path "TestDrive:\$file2" | Should -be "This is content" 90 | } 91 | 92 | It "The filesystem provider should support ShouldProcess (reference ProviderSupportsShouldProcess member)" { 93 | $cci = ((Get-Command -Name Clear-Content).ImplementingType)::new() 94 | $cci.SupportsShouldProcess | Should -BeTrue 95 | } 96 | 97 | } 98 | Context "Proper errors should be delivered when bad locations are specified" { 99 | It "should throw when targeting a directory." { 100 | New-Item -Path TestDrive:\newDirectory -ItemType Directory -Force 101 | { Clear-Content -Path TestDrive:\newDirectory -ErrorAction Stop } | Should -Throw -ErrorId "ClearDirectoryContent,Microsoft.PowerShell.Commands.ClearContentCommand" 102 | } 103 | 104 | It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$null" { 105 | { Clear-Content -Path $null -ErrorAction Stop } | 106 | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ClearContentCommand" 107 | } 108 | 109 | # This is not applicable to the PS1C provider 110 | #[BugId(BugDatabase.WindowsOutOfBandReleases, 903880)] 111 | #It "should throw `"Cannot bind argument to parameter 'Path'`" when -Path is `$()" { 112 | # { Clear-Content -Path $() -ErrorAction Stop } | 113 | # Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ClearContentCommand" 114 | #} 115 | 116 | # This is not applicable to the PS1C provider 117 | #[DRT][BugId(BugDatabase.WindowsOutOfBandReleases, 906022)] 118 | # It "should throw 'PSNotSupportedException' when you clear-content to an unsupported provider" { 119 | # $functionName = Get-NonExistantFunctionName 120 | # $null = New-Item -Path function:$functionName -Value { 1 } 121 | # { Clear-Content -Path function:$functionName -ErrorAction Stop } | 122 | # Should -Throw -ErrorId "NotSupported,Microsoft.PowerShell.Commands.ClearContentCommand" 123 | # } 124 | 125 | It "should throw FileNotFound error when referencing a non-existant file" { 126 | $badFile = "TestDrive:/badfilename.txt" 127 | { Clear-Content -Path $badFile -ErrorAction Stop } | 128 | Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.ClearContentCommand" 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /Tests/PS1C/Get-Content.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | Describe "Get-Content" -Tags "CI" { 6 | BeforeAll { 7 | function New-ZipFile { 8 | [CmdletBinding()] 9 | param( 10 | [Parameter(Mandatory)] 11 | [System.String] $Path 12 | ) 13 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 14 | [System.IO.File]::WriteAllBytes($Path, $bytes) 15 | } 16 | 17 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 18 | 19 | $zipfilePath = "$env:TEMP\ZipFile.zip" 20 | New-ZipFile -Path $zipfilePath 21 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 22 | 23 | $TestDrive = "TestDrive:\" 24 | 25 | $testString = "This is a test content for a file" 26 | $nl = [Environment]::NewLine 27 | $firstline = "Here's a first line " 28 | $secondline = " here's a second line" 29 | $thirdline = "more text" 30 | $fourthline = "just to make sure" 31 | $fifthline = "there's plenty to work with" 32 | $testString2 = $firstline + $nl + $secondline + $nl + $thirdline + $nl + $fourthline + $nl + $fifthline 33 | $testPath = Join-Path -Path $TestDrive -ChildPath testfile1 34 | $testPath2 = Join-Path -Path $TestDrive -ChildPath testfile2 35 | $testContent = "AA","BB","CC","DD" 36 | $testDelimiterContent = "Hello1,World1","Hello2,World2","Hello3,World3","Hello4,World4" 37 | 38 | 39 | #$ArchiveFile = "$PSScriptRoot\Tests\ZipFile" 40 | #Import-Module .\Source\PS1C\bin\Debug\netcoreapp3.0\ps1c.dll 41 | #New-PSDrive -Name PSProvider -PSProvider ZipFile -root "$ArchiveFile.zip" -ErrorAction "Stop" 42 | 43 | function Out-PesterMessage { 44 | param ( 45 | [int] $indent = 2, 46 | [Parameter(ValueFromPipeline)] 47 | [object] $InputObject 48 | ) 49 | begin { 50 | $InputObjects = New-Object "System.Collections.Generic.List[object]" 51 | } 52 | process { 53 | # Collect all objects in Pipeline. 54 | $InputObjects.Add($InputObject) 55 | } 56 | end { 57 | $OutputString = $InputObjects | 58 | Out-String | 59 | ForEach-Object Trim | 60 | ForEach-Object Split "`n" | 61 | ForEach-Object { "{0}{1}" -f (" " * 4 * $indent), $_ } | 62 | Write-Host -ForegroundColor Cyan 63 | } 64 | } 65 | 66 | } 67 | 68 | BeforeEach { 69 | New-Item -Path $testPath -ItemType file -Force -Value $testString 70 | New-Item -Path $testPath2 -ItemType file -Force -Value $testString2 71 | } 72 | 73 | AfterEach { 74 | Remove-Item -Path $testPath -Force 75 | Remove-Item -Path $testPath2 -Force 76 | } 77 | 78 | It "Should throw an error on a directory" { 79 | { Get-Content . -ErrorAction Stop } | 80 | Should -Throw -ErrorId "GetContainerContentException,Microsoft.PowerShell.Commands.GetContentCommand" 81 | } 82 | 83 | It "Should return an Object when listing only a single line and the correct information from a file" { 84 | $content = (Get-Content -Path $testPath) 85 | $content | Should -BeExactly $testString 86 | $content.Count | Should -Be 1 87 | $content | Should -BeOfType "System.String" 88 | } 89 | 90 | It "Should deliver an array object when listing a file with multiple lines and the correct information from a file" { 91 | $content = (Get-Content -Path $testPath2) 92 | @(Compare-Object $content $testString2.Split($nl) -SyncWindow 0).Length | Should -Be 0 93 | ,$content | Should -BeOfType "System.Array" 94 | } 95 | 96 | It "Should be able to return a specific line from a file" { 97 | (Get-Content -Path $testPath2)[1] | Should -BeExactly $secondline 98 | } 99 | 100 | It "Should be able to specify the number of lines to get the content of using the TotalCount switch" { 101 | $returnArray = (Get-Content -Path $testPath2 -TotalCount 2) 102 | $returnArray[0] | Should -BeExactly $firstline 103 | $returnArray[1] | Should -BeExactly $secondline 104 | } 105 | 106 | It "Should be able to specify the number of lines to get the content of using the Head switch" { 107 | $returnArray = (Get-Content -Path $testPath2 -Head 2) 108 | $returnArray[0] | Should -BeExactly $firstline 109 | $returnArray[1] | Should -BeExactly $secondline 110 | } 111 | 112 | It "Should be able to specify the number of lines to get the content of using the First switch" { 113 | $returnArray = (Get-Content -Path $testPath2 -First 2) 114 | $returnArray[0] | Should -BeExactly $firstline 115 | $returnArray[1] | Should -BeExactly $secondline 116 | } 117 | 118 | It "Should return the last line of a file using the Tail switch" { 119 | #Get-Content -Path $testPath -Tail 1 | Should -BeExactly $testString 120 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 121 | } 122 | 123 | It "Should return the last lines of a file using the Last alias" { 124 | # Get-Content -Path $testPath2 -Last 1 | Should -BeExactly $fifthline 125 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 126 | 127 | } 128 | 129 | # It "Should be able to get content within a different drive" { 130 | # Push-Location env: 131 | # $expectedoutput = [Environment]::GetEnvironmentVariable("PATH"); 132 | # { Get-Content PATH } | Should -Not -Throw 133 | # Get-Content PATH | Should -BeExactly $expectedoutput 134 | # Pop-Location 135 | # } 136 | 137 | # [BugId(BugDatabase.WindowsOutOfBandReleases, 906022)] 138 | # It "should throw 'PSNotSupportedException' when you Set-Content to an unsupported provider" -Skip:($IsLinux -Or $IsMacOS) { 139 | # {Get-Content -Path HKLM:\\software\\microsoft -ErrorAction Stop} | Should -Throw "IContentCmdletProvider interface is not implemented" 140 | # } 141 | 142 | # It 'Verifies -Tail reports a TailNotSupported error for unsupported providers' { 143 | # {Get-Content -Path Variable:\PSHOME -Tail 1 -ErrorAction Stop} | Should -Throw -ErrorId 'TailNotSupported,Microsoft.PowerShell.Commands.GetContentCommand' 144 | # } 145 | 146 | It 'Verifies using -Tail and -TotalCount together reports a TailAndHeadCannotCoexist error' { 147 | # { Get-Content -Path Variable:\PSHOME -Tail 1 -TotalCount 5 -ErrorAction Stop} | Should -Throw -ErrorId 'TailAndHeadCannotCoexist,Microsoft.PowerShell.Commands.GetContentCommand' 148 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 149 | } 150 | 151 | It 'Verifies -Tail with content that uses an explicit encoding' -TestCases @( 152 | @{EncodingName = 'String'}, 153 | @{EncodingName = 'Unicode'}, 154 | @{EncodingName = 'BigEndianUnicode'}, 155 | @{EncodingName = 'UTF8'}, 156 | @{EncodingName = 'UTF7'}, 157 | @{EncodingName = 'UTF32'}, 158 | @{EncodingName = 'Ascii'} 159 | ){ 160 | param($EncodingName) 161 | 162 | $content = @" 163 | one 164 | two 165 | foo 166 | bar 167 | baz 168 | "@ 169 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 170 | 171 | # $expected = 'foo' 172 | # $tailCount = 3 173 | 174 | # $testPath = Join-Path -Path $TestDrive -ChildPath 'TailWithEncoding.txt' 175 | # $content | Set-Content -Path $testPath -Encoding $encodingName 176 | # $expected = 'foo' 177 | 178 | # $actual = Get-Content -Path $testPath -Tail $tailCount -Encoding $encodingName 179 | # $actual | Should -BeOfType [string] 180 | # $actual.Length | Should -Be $tailCount 181 | # $actual[0] | Should -BeExactly $expected 182 | } 183 | 184 | It "should Get-Content with a variety of -Tail and -ReadCount: " -TestCases @( 185 | @{ test = "negative readcount" 186 | GetContentParams = @{Path = $testPath; Readcount = -1; Tail = 5} 187 | expectedLength = 4 188 | expectedContent = "AA","BB","CC","DD" 189 | } 190 | @{ test = "readcount=0" 191 | GetContentParams = @{Path = $testPath; Readcount = 0; Tail = 3} 192 | expectedLength = 3 193 | expectedContent = "BB","CC","DD" 194 | } 195 | @{ test = "readcount=1" 196 | GetContentParams = @{Path = $testPath; Readcount = 1; Tail = 3} 197 | expectedLength = 3 198 | expectedContent = "BB","CC","DD" 199 | } 200 | @{ test = "high readcount" 201 | GetContentParams = @{Path = $testPath; Readcount = 99999; Tail = 3} 202 | expectedLength = 3 203 | expectedContent = "BB","CC","DD" 204 | } 205 | @{ test = "readcount=2 tail=3" 206 | GetContentParams = @{Path = $testPath; Readcount = 2; Tail = 3} 207 | expectedLength = 2 208 | expectedContent = ("BB","CC"), "DD" 209 | } 210 | @{ test = "readcount=2 tail=2" 211 | GetContentParams = @{Path = $testPath; Readcount = 2; Tail = 2} 212 | expectedLength = 2 213 | expectedContent = "CC","DD" 214 | } 215 | ) { 216 | param($GetContentParams, $expectedLength, $expectedContent) 217 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 218 | # Set-Content -Path $testPath $testContent 219 | # $result = Get-Content @GetContentParams 220 | # $result.Length | Should -Be $expectedLength 221 | # $result | Should -BeExactly $expectedContent 222 | } 223 | 224 | It "should Get-Content with a variety of -Delimiter and -Tail: " -TestCases @( 225 | @{ test = ", as delimiter" 226 | GetContentParams = @{Path = $testPath; Delimiter = ","; Tail = 2} 227 | expectedLength = 2 228 | expectedContent = "World3${nl}Hello4", "World4${nl}" 229 | } 230 | @{ test = "o as delimiter" 231 | GetContentParams = @{Path = $testPath; Delimiter = "o"; Tail = 3} 232 | expectedLength = 3 233 | expectedContent = "rld3${nl}Hell", '4,W', "rld4${nl}" 234 | } 235 | ) { 236 | param($GetContentParams, $expectedLength, $expectedContent) 237 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 238 | 239 | # Set-Content -Path $testPath $testDelimiterContent 240 | # $result = Get-Content @GetContentParams 241 | # $result.Length | Should -Be $expectedLength 242 | # $result | Should -BeExactly $expectedContent 243 | } 244 | 245 | It "should Get-Content with a variety of -Tail values and -AsByteStream parameter" -TestCases @( 246 | @{ 247 | GetContentParams = @{ 248 | Path = $testPath; 249 | Tail = 10; 250 | # TotalCount = 10; 251 | AsByteStream = $true 252 | } 253 | expectedLength = 10 254 | # Byte encoding of \r\nCC\r\nDD\r\n 255 | expectedWindowsContent = 13, 10, 67, 67, 13, 10, 68, 68, 13, 10 256 | # Byte encoding of \nBB\nCC\nDD\n 257 | expectedNotWindowsContent = 10, 66, 66, 10, 67, 67, 10, 68, 68, 10 258 | } 259 | ) { 260 | param($GetContentParams, $expectedLength, $expectedWindowsContent, $expectedNotWindowsContent) 261 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 262 | 263 | # Set-Content -Path $testPath $testContent 264 | # $result = Get-Content @GetContentParams 265 | # $result.Length | Should -Be $expectedLength 266 | # if ($isWindows) { 267 | # $result | Should -BeExactly $expectedWindowsContent 268 | # } else { 269 | # $result | Should -BeExactly $expectedNotWindowsContent 270 | # } 271 | } 272 | 273 | 274 | #[BugId(BugDatabase.WindowsOutOfBandReleases, 905829)] 275 | It "should Get-Content that matches the input string"{ 276 | Set-Content $testPath "Hello,llllWorlld","Hello2,llllWorlld2" 277 | $result = Get-Content $testPath -Delimiter "ll" 278 | $result.Length | Should -Be 9 279 | 280 | $expected = 'He', 'o,', '', 'Wor', "d${nl}He", 'o2,', '', 'Wor', "d2${nl}" 281 | for ($i = 0; $i -lt $result.Length ; $i++) { $result[$i] | Should -BeExactly $expected[$i]} 282 | } 283 | 284 | # It "Should support NTFS streams using colon syntax" -Skip:(!$IsWindows) { 285 | # Set-Content "${testPath}:Stream" -Value "Foo" 286 | # { Test-Path "${testPath}:Stream" | Should -Throw -ErrorId "ItemExistsNotSupportedError,Microsoft.PowerShell.Commands,TestPathCommand" } 287 | # Get-Content "${testPath}:Stream" | Should -BeExactly "Foo" 288 | # Get-Content $testPath | Should -BeExactly $testString 289 | # } 290 | 291 | # It "Should support NTFS streams using -Stream" -Skip:(!$IsWindows) { 292 | # Set-Content -Path $testPath -Stream hello -Value World 293 | # Get-Content -Path $testPath | Should -BeExactly $testString 294 | # Get-Content -Path $testPath -Stream hello | Should -BeExactly "World" 295 | # $item = Get-Item -Path $testPath -Stream hello 296 | # $item | Should -BeOfType 'System.Management.Automation.Internal.AlternateStreamData' 297 | # $item.Stream | Should -BeExactly "hello" 298 | # Clear-Content -Path $testPath -Stream hello 299 | # Get-Content -Path $testPath -Stream hello | Should -BeNullOrEmpty 300 | # Remove-Item -Path $testPath -Stream hello 301 | # { Get-Content -Path $testPath -Stream hello | Should -Throw -ErrorId "GetContentReaderFileNotFoundError,Microsoft.PowerShell.Commands.GetContentCommand" } 302 | # } 303 | 304 | # It "Should support colons in filename on Linux/Mac" -Skip:($IsWindows) { 305 | # Set-Content "${testPath}:Stream" -Value "Hello" 306 | # "${testPath}:Stream" | Should -Exist 307 | # Get-Content "${testPath}:Stream" | Should -BeExactly "Hello" 308 | # } 309 | 310 | # It "-Stream is not a valid parameter for on Linux/Mac" -Skip:($IsWindows) -TestCases @( 311 | # @{cmdlet="Get-Content"}, 312 | # @{cmdlet="Set-Content"}, 313 | # @{cmdlet="Clear-Content"}, 314 | # @{cmdlet="Add-Content"}, 315 | # @{cmdlet="Get-Item"}, 316 | # @{cmdlet="Remove-Item"} 317 | # ) { 318 | # param($cmdlet) 319 | # (Get-Command $cmdlet).Parameters["stream"] | Should -BeNullOrEmpty 320 | # } 321 | 322 | It "Should return no content when an empty path is used with -Raw switch" { 323 | Set-ItResult -Inconclusive -Because "TODO: is failing due to not implimented yet" 324 | #Get-ChildItem $TestDrive -Filter "*.raw" | Get-Content -Raw | Should -BeNullOrEmpty 325 | } 326 | 327 | It "Should return no content when -TotalCount value is 0" { 328 | Get-Content -Path $testPath -TotalCount 0 | Should -BeNullOrEmpty 329 | } 330 | 331 | 332 | It "Should throw TailAndHeadCannotCoexist when both -Tail and -TotalCount are used" { 333 | { 334 | Get-Content -Path $testPath -Tail 1 -TotalCount 1 -ErrorAction Stop 335 | } | Should -Throw -ErrorId "TailAndHeadCannotCoexist,Microsoft.PowerShell.Commands.GetContentCommand" 336 | } 337 | 338 | It "Should throw InvalidOperation when -Tail and -Raw are used" { 339 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 340 | # { 341 | # Get-Content -Path $testPath -Tail 1 -ErrorAction Stop -Raw 342 | # } | Should -Throw -ErrorId "InvalidOperation,Microsoft.PowerShell.Commands.GetContentCommand" 343 | } 344 | 345 | It "Should throw ItemNotFound when path matches no files with " -TestCases @( 346 | @{ variation = "no additional parameters"; params = @{} }, 347 | @{ variation = "dynamic parameter" ; params = @{ Raw = $true }} 348 | ) { 349 | param($params) 350 | 351 | { Get-Content -Path "/DoesNotExist*.txt" @params -ErrorAction Stop } | Should -Throw -ErrorId "ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand" 352 | } 353 | Context "Check Get-Content containing multi-byte chars" { 354 | BeforeAll { 355 | $firstLine = "Hello,World" 356 | $secondLine = "Hello2,World2" 357 | $thirdLine = "Hello3,World3" 358 | $fourthLine = "Hello4,World4" 359 | $fileContent = $firstLine,$secondLine,$thirdLine,$fourthLine 360 | } 361 | BeforeEach { 362 | Set-Content -Path $testPath $fileContent 363 | } 364 | 365 | It "Should return all lines when -Tail value is more than number of lines in the file" { 366 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 367 | #$result = Get-Content -Path $testPath -ReadCount -1 -Tail 5 -Encoding UTF7 368 | #$result.Length | Should -Be 4 369 | #$expected = $fileContent 370 | #Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 371 | } 372 | 373 | It "Should return last three lines at one time for -ReadCount 0 and -Tail 3" { 374 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 375 | #$result = Get-Content -Path $testPath -ReadCount 0 -Tail 3 -Encoding UTF7 376 | #$result.Length | Should -Be 3 377 | #$expected = $secondLine,$thirdLine,$fourthLine 378 | #Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 379 | } 380 | 381 | It "Should return last three lines reading one at a time for -ReadCount 1 and -Tail 3" { 382 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 383 | #$result = Get-Content -Path $testPath -ReadCount 1 -Tail 3 -Encoding UTF7 384 | # $result.Length | Should -Be 3 385 | # $expected = $secondLine,$thirdLine,$fourthLine 386 | # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 387 | } 388 | 389 | It "Should return last three lines at one time for -ReadCount 99999 and -Tail 3" { 390 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 391 | # $result = Get-Content -Path $testPath -ReadCount 99999 -Tail 3 -Encoding UTF7 392 | # $result.Length | Should -Be 3 393 | # $expected = $secondLine,$thirdLine,$fourthLine 394 | # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 395 | } 396 | 397 | It "Should return last three lines two lines at a time for -ReadCount 2 and -Tail 3" { 398 | Set-ItResult -Inconclusive -Because "-Tail is not supported in custom proviers" 399 | # $result = Get-Content -Path $testPath -ReadCount 2 -Tail 3 -Encoding UTF7 400 | # $result.Length | Should -Be 2 401 | # $expected = New-Object System.Array[] 2 402 | # $expected[0] = ($secondLine,$thirdLine) 403 | # $expected[1] = $fourthLine 404 | # Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 405 | } 406 | 407 | It "Should not return any content when -TotalCount 0" { 408 | $result = Get-Content -Path $testPath -TotalCount 0 -ReadCount 1 -Encoding UTF8 409 | $result.Length | Should -Be 0 410 | } 411 | 412 | It "Should return first three lines two lines at a time for -TotalCount 3 and -ReadCount 2" { 413 | $result = Get-Content -Path $testPath -TotalCount 3 -ReadCount 2 -Encoding UTF8 414 | $result.Length | Should -Be 2 415 | $expected = New-Object System.Array[] 2 416 | $expected[0] = ($firstLine,$secondLine) 417 | $expected[1] = $thirdLine 418 | Compare-Object -ReferenceObject $expected -DifferenceObject $result | Should -BeNullOrEmpty 419 | } 420 | 421 | It "A warning should be emitted if both -AsByteStream and -Encoding are used together" { 422 | [byte[]][char[]]"test" | Set-Content -Encoding Unicode -AsByteStream -Path "${TESTDRIVE}\bfile.txt" -WarningVariable contentWarning *> $null 423 | $contentWarning.Message | Should -Match "-AsByteStream" 424 | } 425 | } 426 | 427 | } -------------------------------------------------------------------------------- /Tests/PS1C/New-Item.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | Describe "New-Item" -Tags "CI" { 5 | BeforeAll { 6 | function New-ZipFile { 7 | [CmdletBinding()] 8 | param( 9 | [Parameter(Mandatory)] 10 | [System.String] $Path 11 | ) 12 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 13 | [System.IO.File]::WriteAllBytes($Path, $bytes) 14 | } 15 | 16 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 17 | $zipfilePath = "$env:TEMP\ZipFile.zip" 18 | New-ZipFile -Path $zipfilePath 19 | 20 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 21 | 22 | $TestDrive = "TestDrive:\" 23 | $tmpDirectory = $TestDrive 24 | $testfile = "testfile.txt" 25 | $testfolder = "newDirectory" 26 | $testsubfolder = "newSubDirectory" 27 | $testlink = "testlink" 28 | $FullyQualifiedFile = Join-Path -Path $tmpDirectory -ChildPath $testfile 29 | $FullyQualifiedFolder = Join-Path -Path $tmpDirectory -ChildPath $testfolder 30 | $FullyQualifiedLink = Join-Path -Path $tmpDirectory -ChildPath $testlink 31 | $FullyQualifiedSubFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testsubfolder 32 | $FullyQualifiedFileInFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testfile 33 | 34 | } 35 | 36 | BeforeEach { 37 | if (Test-Path $FullyQualifiedLink) 38 | { 39 | Remove-Item $FullyQualifiedLink -Force 40 | } 41 | 42 | if (Test-Path $FullyQualifiedFile) 43 | { 44 | Remove-Item $FullyQualifiedFile -Force 45 | } 46 | 47 | if ($FullyQualifiedFileInFolder -and (Test-Path $FullyQualifiedFileInFolder)) 48 | { 49 | Remove-Item $FullyQualifiedFileInFolder -Force 50 | } 51 | 52 | if ($FullyQualifiedSubFolder -and (Test-Path $FullyQualifiedSubFolder)) 53 | { 54 | Remove-Item $FullyQualifiedSubFolder -Force 55 | } 56 | 57 | if (Test-Path $FullyQualifiedFolder) 58 | { 59 | Remove-Item $FullyQualifiedFolder -Force 60 | } 61 | 62 | } 63 | 64 | It "should call the function without error" { 65 | { New-Item -Name $testfile -Path $tmpDirectory -ItemType file } | Should -Not -Throw 66 | } 67 | 68 | It "should call the function without error" { 69 | { New-Item -Name $testfile -Path $tmpDirectory -ItemType file } | Should -Not -Throw 70 | } 71 | 72 | It "Should create a file without error" { 73 | New-Item -Name $testfile -Path $tmpDirectory -ItemType file 74 | 75 | Test-Path $FullyQualifiedFile | Should -BeTrue 76 | 77 | $fileInfo = Get-ChildItem $FullyQualifiedFile 78 | $fileInfo.Target | Should -BeNullOrEmpty 79 | $fileInfo.LinkType | Should -BeNullOrEmpty 80 | } 81 | 82 | It "Should create a folder without an error" { 83 | New-Item -Name newDirectory -Path $tmpDirectory -ItemType directory 84 | Test-Path $FullyQualifiedFolder | Should -BeTrue 85 | } 86 | 87 | It "Should create a file using the ni alias" { 88 | ni -Name $testfile -Path $tmpDirectory -ItemType file 89 | 90 | Test-Path $FullyQualifiedFile | Should -BeTrue 91 | } 92 | 93 | It "Should create a file using the Type alias instead of ItemType" { 94 | New-Item -Name $testfile -Path $tmpDirectory -Type file 95 | 96 | Test-Path $FullyQualifiedFile | Should -BeTrue 97 | } 98 | 99 | It "Should create a file with sample text inside the file using the Value switch" { 100 | $expected = "This is test string" 101 | New-Item -Name $testfile -Path $tmpDirectory -ItemType file -Value $expected 102 | 103 | Test-Path $FullyQualifiedFile | Should -BeTrue 104 | 105 | Get-Content $FullyQualifiedFile | Should -Be $expected 106 | } 107 | 108 | It "Should not create a file when the Name switch is not used and only a directory specified" { 109 | #errorAction used because permissions issue in Windows 110 | 111 | New-Item -Path $tmpDirectory -ItemType file -ErrorAction SilentlyContinue 112 | Test-Path $FullyQualifiedFile | Should -BeFalse 113 | } 114 | 115 | It "Should create a file when the Name switch is not used but a fully qualified path is specified" { 116 | New-Item -Path $FullyQualifiedFile -ItemType file 117 | 118 | Test-Path $FullyQualifiedFile | Should -BeTrue 119 | } 120 | 121 | It "Should be able to create a multiple items in different directories" { 122 | $FullyQualifiedFile2 = Join-Path -Path $tmpDirectory -ChildPath test2.txt 123 | New-Item -ItemType file -Path $FullyQualifiedFile, $FullyQualifiedFile2 124 | 125 | Test-Path $FullyQualifiedFile | Should -BeTrue 126 | Test-Path $FullyQualifiedFile2 | Should -BeTrue 127 | 128 | Remove-Item $FullyQualifiedFile2 129 | } 130 | 131 | It "Should be able to call the whatif switch without error" { 132 | { New-Item -Name testfile.txt -Path $tmpDirectory -ItemType file -WhatIf } | Should -Not -Throw 133 | } 134 | 135 | It "Should not create a new file when the whatif switch is used" { 136 | New-Item -Name $testfile -Path $tmpDirectory -ItemType file -WhatIf 137 | 138 | Test-Path $FullyQualifiedFile | Should -BeFalse 139 | } 140 | 141 | It "Should create a file at the root of the drive while the current working directory is not the root" { 142 | try { 143 | New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null 144 | Push-Location -Path "TestDrive:\$testfolder" 145 | New-Item -Name $testfile -Path "TestDrive:\" -ItemType file > $null 146 | Test-Path $FullyQualifiedFile | Should -BeTrue 147 | #Code changed pester for some odd reason dosnt like Should -Exist 148 | #$FullyQualifiedFile | Should -Exist 149 | } 150 | finally { 151 | Pop-Location 152 | 153 | } 154 | } 155 | 156 | It "Should create a folder at the root of the drive while the current working directory is not the root" { 157 | $testfolder2 = "newDirectory2" 158 | $FullyQualifiedFolder2 = Join-Path -Path $tmpDirectory -ChildPath $testfolder2 159 | 160 | try { 161 | New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null 162 | Push-Location -Path "TestDrive:\$testfolder" 163 | New-Item -Name $testfolder2 -Path "TestDrive:\" -ItemType directory > $null 164 | Test-Path $FullyQualifiedFolder2 | Should -BeTrue 165 | #Code changed pester for some odd reason dosnt like Should -Exist 166 | #$FullyQualifiedFolder2 | Should -Exist 167 | 168 | } 169 | finally { 170 | Pop-Location 171 | 172 | #Fixed a bug where cleanup wasnt happening 173 | Remove-Item $FullyQualifiedFolder2 -Force 174 | 175 | } 176 | } 177 | 178 | It "Should create a file in the current directory when using Drive: notation" { 179 | try { 180 | New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null 181 | Push-Location -Path "TestDrive:\$testfolder" 182 | New-Item -Name $testfile -Path "TestDrive:" -ItemType file > $null 183 | 184 | Test-Path $FullyQualifiedFileInFolder | Should -BeTrue 185 | #Code changed pester for some odd reason dosnt like Should -Exist 186 | #$FullyQualifiedFileInFolder | Should -Exist 187 | } 188 | finally { 189 | Pop-Location 190 | } 191 | } 192 | 193 | It "Should create a folder in the current directory when using Drive: notation" { 194 | try { 195 | New-Item -Name $testfolder -Path "TestDrive:\" -ItemType directory > $null 196 | Push-Location -Path "TestDrive:\$testfolder" 197 | New-Item -Name $testsubfolder -Path "TestDrive:" -ItemType file > $null 198 | Test-Path $FullyQualifiedSubFolder | Should -BeTrue 199 | #Code changed pester for some odd reason dosnt like Should -Exist 200 | #$FullyQualifiedSubFolder | Should -Exist 201 | } 202 | finally { 203 | Pop-Location 204 | } 205 | } 206 | } 207 | 208 | # More precisely these tests require SeCreateSymbolicLinkPrivilege. 209 | # You can see list of priveledges with `whoami /priv`. 210 | # In the default windows setup, Admin user has this priveledge, but regular users don't. 211 | 212 | Describe "New-Item -Force allows to create an item even if the directories in the path don't exist" -Tags "CI" { 213 | BeforeAll { 214 | $TestDrive = "TestDrive:\" 215 | $testFile = 'testfile.txt' 216 | $testFolder = 'testfolder' 217 | $FullyQualifiedFolder = Join-Path -Path $TestDrive -ChildPath $testFolder 218 | $FullyQualifiedFile = Join-Path -Path $TestDrive -ChildPath $testFolder -AdditionalChildPath $testFile 219 | } 220 | 221 | BeforeEach { 222 | # Explicitly removing folder and the file before tests 223 | Remove-Item $FullyQualifiedFolder -Recurse -ErrorAction SilentlyContinue 224 | Remove-Item $FullyQualifiedFile -Recurse -ErrorAction SilentlyContinue 225 | Test-Path -Path $FullyQualifiedFolder | Should -BeFalse 226 | Test-Path -Path $FullyQualifiedFile | Should -BeFalse 227 | } 228 | 229 | It "Should error correctly when -Force is not used and folder in the path doesn't exist" { 230 | { New-Item $FullyQualifiedFile -ErrorAction Stop } | Should -Throw -ErrorId 'NewItemIOError,Microsoft.PowerShell.Commands.NewItemCommand' 231 | Test-Path $FullyQualifiedFile | Should -BeFalse 232 | } 233 | It "Should create new file correctly when -Force is used and folder in the path doesn't exist" { 234 | { New-Item $FullyQualifiedFile -Force -ErrorAction Stop } | Should -Not -Throw 235 | Test-Path $FullyQualifiedFile | Should -BeTrue 236 | } 237 | } 238 | 239 | -------------------------------------------------------------------------------- /Tests/PS1C/Remove-Item.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Remove-Item" -Tags "CI" { 5 | BeforeAll { 6 | function New-ZipFile { 7 | [CmdletBinding()] 8 | param( 9 | [Parameter(Mandatory)] 10 | [System.String] $Path 11 | ) 12 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 13 | [System.IO.File]::WriteAllBytes($Path, $bytes) 14 | } 15 | 16 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 17 | 18 | $zipfilePath = "$env:TEMP\ZipFile.zip" 19 | New-ZipFile -Path $zipfilePath 20 | 21 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 22 | 23 | $TestDrive = "TestDrive:\" 24 | $testpath = $TestDrive 25 | $testfile = "testfile.txt" 26 | $testfilepath = Join-Path -Path $testpath -ChildPath $testfile 27 | } 28 | 29 | AfterAll { 30 | 31 | } 32 | Context "File removal Tests" { 33 | BeforeEach { 34 | New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 35 | 36 | Test-Path $testfilepath | Should -BeTrue 37 | } 38 | 39 | It "Should be able to be called on a regular file without error using the Path parameter" { 40 | { Remove-Item -Path $testfilepath } | Should -Not -Throw 41 | 42 | Test-Path $testfilepath | Should -BeFalse 43 | } 44 | It "Should be able to be called on a file without the Path parameter" { 45 | { Remove-Item $testfilepath } | Should -Not -Throw 46 | 47 | Test-Path $testfilepath | Should -BeFalse 48 | } 49 | 50 | It "Should be able to call the rm alias" -Skip:($IsLinux -Or $IsMacOS) { 51 | { rm $testfilepath } | Should -Not -Throw 52 | 53 | Test-Path $testfilepath | Should -BeFalse 54 | } 55 | 56 | It "Should be able to call the del alias" { 57 | { del $testfilepath } | Should -Not -Throw 58 | 59 | Test-Path $testfilepath | Should -BeFalse 60 | } 61 | 62 | It "Should be able to call the erase alias" { 63 | { erase $testfilepath } | Should -Not -Throw 64 | 65 | Test-Path $testfilepath | Should -BeFalse 66 | } 67 | 68 | It "Should be able to call the ri alias" { 69 | { ri $testfilepath } | Should -Not -Throw 70 | 71 | Test-Path $testfilepath | Should -BeFalse 72 | } 73 | 74 | It "Should be able to remove all files matching a regular expression with the include parameter" { 75 | # Create multiple files with specific string 76 | New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 77 | New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 78 | New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 79 | # Create a single file that does not match that string - already done in BeforeEach 80 | 81 | # Delete the specific string 82 | Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt 83 | # validate that the string under test was deleted, and the nonmatching strings still exist 84 | Test-path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse 85 | Test-path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse 86 | Test-path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse 87 | Test-Path $testfilepath | Should -BeTrue 88 | 89 | # Delete the non-matching strings 90 | 91 | Remove-Item $testfilepath -Recurse 92 | 93 | Test-Path $testfilepath | Should -BeFalse 94 | } 95 | 96 | It "Should be able to not remove any files matching a regular expression with the exclude parameter" { 97 | # Create multiple files with specific string 98 | New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 99 | New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force 100 | 101 | # Create a single file that does not match that string 102 | New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" 103 | 104 | # Delete the specific string 105 | Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt 106 | 107 | # validate that the string under test was deleted, and the nonmatching strings still exist 108 | Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue 109 | Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue 110 | Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse 111 | 112 | # Delete the non-matching strings 113 | Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav) 114 | Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav) 115 | 116 | Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse 117 | Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse 118 | } 119 | } 120 | 121 | Context "Directory Removal Tests" { 122 | BeforeAll { 123 | $testdirectory = Join-Path -Path $testpath -ChildPath testdir 124 | $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd 125 | } 126 | 127 | BeforeEach { 128 | New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force 129 | 130 | Test-Path $testdirectory | Should -BeTrue 131 | } 132 | 133 | It "Should be able to remove a directory" { 134 | { Remove-Item $testdirectory } | Should -Not -Throw 135 | 136 | Test-Path $testdirectory | Should -BeFalse 137 | } 138 | 139 | It "Should be able to recursively delete subfolders" { 140 | New-Item -Name "subd" -Path $testdirectory -ItemType "directory" 141 | New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum" 142 | 143 | $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile 144 | test-path $complexDirectory | Should -BeTrue 145 | 146 | { Remove-Item $testdirectory -Recurse} | Should -Not -Throw 147 | 148 | Test-Path $testdirectory | Should -BeFalse 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Tests/PS1C/Rename-Item.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Rename-Item tests" -Tag "CI" { 5 | BeforeAll { 6 | function New-ZipFile { 7 | [CmdletBinding()] 8 | param( 9 | [Parameter(Mandatory)] 10 | [System.String] $Path 11 | ) 12 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 13 | [System.IO.File]::WriteAllBytes($Path, $bytes) 14 | } 15 | 16 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 17 | 18 | $zipfilePath = "$env:TEMP\ZipFile.zip" 19 | New-ZipFile -Path $zipfilePath 20 | 21 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 22 | 23 | $TestDrive = "TestDrive:" 24 | 25 | $source = "$TESTDRIVE/originalFile.txt" 26 | $target = "$TESTDRIVE/ItemWhichHasBeenRenamed.txt" 27 | 28 | $sourceSp = "$TestDrive/``[orig-file``].txt" 29 | $targetSpName = "ItemWhichHasBeen[Renamed].txt" 30 | $targetSp = "$TestDrive/ItemWhichHasBeen``[Renamed``].txt" 31 | # Setup -Dir [test-dir] 32 | $wdSp = "$TestDrive/``[test-dir``]" 33 | 34 | # Setup file System 35 | New-Item $Source -Value "This is content" -Force -ErrorAction stop 36 | 37 | try { 38 | New-Item -Path $sourceSP -Value "This is not content" -Force -ErrorAction Continue 39 | New-Item "$wdSp" -ItemType Directory -ErrorAction Continue 40 | } 41 | catch { 42 | 43 | } 44 | 45 | } 46 | AfterAll { 47 | try { 48 | Remove-Item $target -Force -ErrorAction Continue 49 | Remove-Item $targetSpName -Force -ErrorAction Continue 50 | } 51 | catch {} 52 | } 53 | It "Rename-Item will rename a file" { 54 | Rename-Item $source $target 55 | 56 | Test-Path $source | Should -BeFalse 57 | Test-Path $target | Should -BeTrue 58 | 59 | Get-Content $target | should -Be "This is content" 60 | } 61 | It "Rename-Item will rename a file when path contains special char" { 62 | Rename-Item $sourceSp $targetSpName 63 | test-path $sourceSp | Should -BeFalse 64 | test-path -path $targetSp | Should -true 65 | 66 | Get-Content $targetSp | should -Be "This is content" 67 | } 68 | # It "Rename-Item will rename a file when -Path and CWD contains special char" { 69 | # $content = "This is content" 70 | # $oldSpName = "[orig]file.txt" 71 | # $oldSpBName = "``[orig``]file.txt" 72 | # $oldSp = "$wdSp/$oldSpBName" 73 | # $newSpName = "[renamed]file.txt" 74 | # $newSp = "$wdSp/``[renamed``]file.txt" 75 | # In $wdSp -Execute { 76 | # $null = New-Item -Name $oldSpName -ItemType File -Value $content -Force 77 | # Rename-Item -Path $oldSpBName $newSpName 78 | # } 79 | # $oldSp | Should -Not -Exist 80 | # $newSp | Should -Exist 81 | # $newSp | Should -FileContentMatchExactly $content 82 | # } 83 | # It "Rename-Item will rename a file when -LiteralPath and CWD contains special char" { 84 | # $content = "This is not content" 85 | # $oldSpName = "[orig]file2.txt" 86 | # $oldSpBName = "``[orig``]file2.txt" 87 | # $oldSp = "$wdSp/$oldSpBName" 88 | # $newSpName = "[renamed]file2.txt" 89 | # $newSp = "$wdSp/``[renamed``]file2.txt" 90 | # In $wdSp -Execute { 91 | # $null = New-Item -Name $oldSpName -ItemType File -Value $content -Force 92 | # Rename-Item -LiteralPath $oldSpName $newSpName 93 | # } 94 | # $oldSp | Should -Not -Exist 95 | # $newSp | Should -Exist 96 | # $newSp | Should -FileContentMatchExactly $content 97 | # } 98 | } -------------------------------------------------------------------------------- /Tests/PS1C/Set-Content.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Set-Content cmdlet tests" -Tags "CI" { 5 | BeforeAll { 6 | function New-ZipFile { 7 | [CmdletBinding()] 8 | param( 9 | [Parameter(Mandatory)] 10 | [System.String] $Path 11 | ) 12 | $bytes = [System.Convert]::FromBase64String("UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==") 13 | [System.IO.File]::WriteAllBytes($Path, $bytes) 14 | } 15 | 16 | Import-Module "$PSScriptRoot\..\..\PS1C\PS1C.psd1" 17 | 18 | $zipfilePath = "$env:TEMP\ZipFile.zip" 19 | New-ZipFile -Path $zipfilePath 20 | 21 | New-PSDrive -Name TestDrive -PSProvider PS1C -root "$zipfilePath" -ErrorAction "Stop" 22 | 23 | $testdrive = "TestDrive:\" 24 | 25 | $file1 = "file1.txt" 26 | $filePath1 = Join-Path $testdrive $file1 27 | 28 | try { New-Item TestDrive:\bfile.txt -Value "" -ErrorAction Continue } 29 | catch {} 30 | } 31 | 32 | It "A warning should be emitted if both -AsByteStream and -Encoding are used together" { 33 | $testfile = "${TESTDRIVE}\bfile.txt" 34 | "test" | Set-Content $testfile 35 | $result = Get-Content -AsByteStream -Encoding Unicode -Path $testfile -WarningVariable contentWarning *> $null 36 | $contentWarning.Message | Should -Match "-AsByteStream" 37 | } 38 | 39 | Context "Set-Content should create a file if it does not exist" { 40 | AfterEach { 41 | Remove-Item -Path $filePath1 -Force -ErrorAction SilentlyContinue 42 | } 43 | It "should create a file if it does not exist" { 44 | Set-Content -Path $filePath1 -Value "$file1" -force 45 | $result = Get-Content -Path $filePath1 46 | $result| Should -Be "$file1" 47 | } 48 | } 49 | Context "Set-Content/Get-Content should set/get the content of an exisiting file" { 50 | BeforeAll { 51 | New-Item -Path $filePath1 -ItemType File -Force 52 | } 53 | It "should set-Content of testdrive\$file1" { 54 | Set-Content -Path $filePath1 -Value "ExpectedContent" 55 | $result = Get-Content -Path $filePath1 56 | $result| Should -Be "ExpectedContent" 57 | } 58 | It "should return expected string from testdrive\$file1" { 59 | $result = Get-Content -Path $filePath1 60 | $result | Should -BeExactly "ExpectedContent" 61 | } 62 | It "should Set-Content to testdrive:\dynamicfile.txt with dynamic parameters" { 63 | Set-Content -Path $testdrive\dynamicfile.txt -Value "ExpectedContent" -Force:$true 64 | $result = Get-Content -Path $testdrive\dynamicfile.txt 65 | $result | Should -BeExactly "ExpectedContent" 66 | } 67 | It "should return expected string from testdrive\dynamicfile.txt" { 68 | $result = Get-Content -Path $testdrive\dynamicfile.txt 69 | $result | Should -BeExactly "ExpectedContent" 70 | } 71 | It "should remove existing content from testdrive\$file1 when the -Value is `$null" { 72 | $AsItWas = Get-Content $filePath1 73 | $AsItWas | Should -BeExactly "ExpectedContent" 74 | Set-Content -Path $filePath1 -Value $null -ErrorAction Stop 75 | $AsItIs = Get-Content $filePath1 76 | $AsItIs | Should -Not -Be $AsItWas 77 | } 78 | It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$null" { 79 | { Set-Content -Path $null -Value "ShouldNotWorkBecausePathIsNull" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SetContentCommand" 80 | } 81 | It "should throw 'ParameterArgumentValidationErrorNullNotAllowed' when -Path is `$()" { 82 | { Set-Content -Path $() -Value "ShouldNotWorkBecausePathIsInvalid" -ErrorAction Stop } | Should -Throw -ErrorId "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SetContentCommand" 83 | } 84 | #[BugId(BugDatabase.WindowsOutOfBandReleases, 9058182)] 85 | It "should be able to pass multiple [string]`$objects to Set-Content through the pipeline to output a dynamic Path file" { 86 | "hello","world"|Set-Content $testdrive\dynamicfile2.txt -Force 87 | $result=Get-Content $testdrive\dynamicfile2.txt 88 | $result.length | Should -Be 2 89 | $result[0] | Should -BeExactly "hello" 90 | $result[1] | Should -BeExactly "world" 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Tests/Test.ps1: -------------------------------------------------------------------------------- 1 | #Import-Module ..\..\..\ps1c.psm1 2 | 3 | $path = (Get-Item $PSScriptRoot\..\PS1C\).FullName 4 | 5 | if ($env:PSModulePath -notlike "*$path*") { 6 | $env:PSModulePath += ";$path" 7 | } 8 | 9 | Write-Host "Importing module PS1C" 10 | Import-Module $path\PS1C.psd1 -Force 11 | 12 | # Pester Tests 13 | $tests = @( 14 | "$PSScriptRoot\PS1C\Add-Content.Tests.ps1", # Successful 15 | "$PSScriptRoot\PS1C\Clear-Content.Tests.ps1", # Successful 16 | "$PSScriptRoot\PS1C\Get-Content.Tests.ps1", # Successful / Disabled UTF7 support 17 | # A warning should be emitted if both -AsByteStream and -Encoding are used together 18 | 19 | "$PSScriptRoot\PS1C\New-Item.Tests.ps1", # Successful 20 | "$PSScriptRoot\PS1C\Remove-Item.Tests.ps1" # Successful 21 | # Rename-Item will rename a file when path contains special char 22 | "$PSScriptRoot\PS1c\Rename-Item.Tests.ps1", # Successful 23 | "$PSScriptRoot\PS1C\Set-Content.Tests.ps1" # Successful 24 | # All of Set-Content should create a file if it does not exist without -Force option 25 | ) 26 | 27 | Invoke-Pester -Script $tests -Output Detailed -PassThru 28 | 29 | Remove-Module PS1C -------------------------------------------------------------------------------- /docs/Development/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | * VSCode (Is the preferred method) 3 | * DotNet SDK 8.0 4 | * Nuget Repository Configured 5 | * Install Module Pester 6 | ``` ps1 7 | Install-Module Pester -Force 8 | ``` 9 | 10 | # Build the code 11 | 12 | Running `dotnet build` from the root of the project will build the project 13 | 14 | ``` ps1 15 | # Build the code 16 | dotnet build 17 | ``` 18 | 19 | # Pester Test the code 20 | 21 | ``` ps1 22 | . .\Tests\Test.ps1 23 | ``` 24 | 25 | # Custom Tests and Debugging 26 | 27 | ``` ps1 28 | 29 | ``` -------------------------------------------------------------------------------- /docs/cmdlets/Add-Content.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romero126/PS1C/57cb36c41ead94aff080772f94f42cbf2c95734e/docs/cmdlets/Add-Content.md -------------------------------------------------------------------------------- /docs/cmdlets/Clear-Content.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romero126/PS1C/57cb36c41ead94aff080772f94f42cbf2c95734e/docs/cmdlets/Clear-Content.md -------------------------------------------------------------------------------- /docs/cmdlets/Get-Content.md: -------------------------------------------------------------------------------- 1 | 2 | # Limitations 3 | Tail is disabled due to a limitation in PowerShell 4 | -------------------------------------------------------------------------------- /docs/cmdlets/New-Item.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romero126/PS1C/57cb36c41ead94aff080772f94f42cbf2c95734e/docs/cmdlets/New-Item.md -------------------------------------------------------------------------------- /docs/cmdlets/Remove-Item.md: -------------------------------------------------------------------------------- 1 | Remove-Item regex support is not available at this time -------------------------------------------------------------------------------- /docs/cmdlets/Rename-Item.md: -------------------------------------------------------------------------------- 1 | # Limitations 2 | Regex support is not available at this time. -------------------------------------------------------------------------------- /docs/cmdlets/Set-Content.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romero126/PS1C/57cb36c41ead94aff080772f94f42cbf2c95734e/docs/cmdlets/Set-Content.md -------------------------------------------------------------------------------- /src/ResGen/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Text; 8 | using System.Xml.Linq; 9 | 10 | namespace ConsoleApplication 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | IEnumerable dirs; 17 | string fileFilter; 18 | 19 | if (args.Length == 1) 20 | { 21 | // We are assuming resgen is run with 'dotnet run pathToResxFile.resx'. 22 | fileFilter = Path.GetFileName(args[0]); 23 | string moduleDirectory = Path.GetDirectoryName(Path.GetDirectoryName(args[0])); 24 | dirs = new List() { moduleDirectory }; 25 | } 26 | else 27 | { 28 | // We are assuming resgen is run with 'dotnet run' 29 | // so we can use relative path to get a parent directory 30 | // to process all *.resx files in all project subdirectories. 31 | fileFilter = "*.resx"; 32 | dirs = Directory.EnumerateDirectories(".."); 33 | } 34 | 35 | foreach (string folder in dirs) 36 | { 37 | string moduleName = Path.GetFileName(folder); 38 | string resourcePath = Path.Combine(folder, "resources"); 39 | 40 | if (Directory.Exists(resourcePath)) 41 | { 42 | string genFolder = Path.Combine(folder, "gen"); 43 | if (!Directory.Exists(genFolder)) 44 | { 45 | Directory.CreateDirectory(genFolder); 46 | } 47 | 48 | foreach (string resxPath in Directory.EnumerateFiles(resourcePath, fileFilter)) 49 | { 50 | string className = Path.GetFileNameWithoutExtension(resxPath); 51 | string sourceCode = GetStronglyTypeCsFileForResx(resxPath, moduleName, className); 52 | string outPath = Path.Combine(genFolder, className + ".cs"); 53 | Console.WriteLine("ResGen for " + outPath); 54 | File.WriteAllText(outPath, sourceCode); 55 | } 56 | } 57 | } 58 | } 59 | 60 | private static string GetStronglyTypeCsFileForResx(string xmlPath, string moduleName, string className) 61 | { 62 | // Example 63 | // 64 | // className = Full.Name.Of.The.ClassFoo 65 | // shortClassName = ClassFoo 66 | // namespaceName = Full.Name.Of.The 67 | 68 | string shortClassName = className; 69 | string namespaceName = null; 70 | int lastIndexOfDot = className.LastIndexOf('.'); 71 | if (lastIndexOfDot != -1) 72 | { 73 | namespaceName = className.Substring(0, lastIndexOfDot); 74 | shortClassName = className.Substring(lastIndexOfDot + 1); 75 | } 76 | 77 | var entries = new StringBuilder(); 78 | XElement root = XElement.Parse(File.ReadAllText(xmlPath)); 79 | foreach (var data in root.Elements("data")) 80 | { 81 | string value = data.Value.Replace("\n", "\n ///"); 82 | string name = data.Attribute("name").Value.Replace(' ', '_'); 83 | entries.AppendFormat(ENTRY, name, value); 84 | } 85 | 86 | string bodyCode = string.Format(BODY, shortClassName, moduleName, entries.ToString(), className); 87 | if (namespaceName != null) 88 | { 89 | bodyCode = string.Format(NAMESPACE, namespaceName, bodyCode); 90 | } 91 | 92 | string resultCode = string.Format(BANNER, bodyCode).Replace("\r\n?|\n", "\r\n"); 93 | return resultCode; 94 | } 95 | 96 | private static readonly string BANNER = @" 97 | //------------------------------------------------------------------------------ 98 | // 99 | // This code was generated by a dotnet run from src\ResGen folder. 100 | // To add or remove a member, edit your .resx file then rerun src\ResGen. 101 | // 102 | // Changes to this file may cause incorrect behavior and will be lost if 103 | // the code is regenerated. 104 | // 105 | //------------------------------------------------------------------------------ 106 | 107 | {0} 108 | "; 109 | 110 | private static readonly string NAMESPACE = @" 111 | namespace {0} {{ 112 | {1} 113 | }} 114 | "; 115 | private static readonly string BODY = @" 116 | using System; 117 | using System.Reflection; 118 | 119 | /// 120 | /// A strongly-typed resource class, for looking up localized strings, etc. 121 | /// 122 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""System.Resources.Tools.StronglyTypedResourceBuilder"", ""4.0.0.0"")] 123 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 124 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 125 | 126 | internal class {0} {{ 127 | 128 | private static global::System.Resources.ResourceManager resourceMan; 129 | 130 | private static global::System.Globalization.CultureInfo resourceCulture; 131 | 132 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(""Microsoft.Performance"", ""CA1811:AvoidUncalledPrivateCode"")] 133 | internal {0}() {{ 134 | }} 135 | 136 | /// 137 | /// Returns the cached ResourceManager instance used by this class. 138 | /// 139 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 140 | internal static global::System.Resources.ResourceManager ResourceManager {{ 141 | get {{ 142 | if (object.ReferenceEquals(resourceMan, null)) {{ 143 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager(""{1}.resources.{3}"", typeof({0}).Assembly); 144 | resourceMan = temp; 145 | }} 146 | 147 | return resourceMan; 148 | }} 149 | }} 150 | 151 | /// 152 | /// Overrides the current threads CurrentUICulture property for all 153 | /// resource lookups using this strongly typed resource class. 154 | /// 155 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 156 | internal static global::System.Globalization.CultureInfo Culture {{ 157 | get {{ 158 | return resourceCulture; 159 | }} 160 | 161 | set {{ 162 | resourceCulture = value; 163 | }} 164 | }} 165 | {2} 166 | }} 167 | "; 168 | 169 | private static readonly string ENTRY = @" 170 | 171 | /// 172 | /// Looks up a localized string similar to {1} 173 | /// 174 | internal static string {0} {{ 175 | get {{ 176 | return ResourceManager.GetString(""{0}"", resourceCulture); 177 | }} 178 | }} 179 | "; 180 | 181 | } 182 | } -------------------------------------------------------------------------------- /src/ResGen/ResGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generates C# typed bindings for .resx files 5 | netcoreapp8.0 6 | resgen 7 | Exe 8 | true 9 | true 10 | 11 | linux-x64;osx-x64;win; 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ps1c/ArchiveContentStream.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.PowerShell.Commands; 3 | using System; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Management.Automation; 7 | using System.Management.Automation.Provider; 8 | using System.Text; 9 | 10 | namespace PS1C 11 | { 12 | #region StreamContent 13 | 14 | #region ArchiveContentStream 15 | public class ArchiveContentStream : StreamContentReaderWriter 16 | { 17 | 18 | private ArchiveItemInfo _archiveFileInfo; 19 | private ArchiveItemStream _archiveFileStream; 20 | 21 | private ArchiveItemStream stream; 22 | private CmdletProvider _provider; 23 | 24 | 25 | public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream) 26 | : base( archiveFileInfo.Open(mode), encoding, usingByteEncoding, provider, isRawStream) 27 | { 28 | _provider = provider; 29 | } 30 | 31 | public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream, bool suppressNewline) 32 | : base(archiveFileInfo.Open(mode), encoding, usingByteEncoding, provider, isRawStream, suppressNewline) 33 | { 34 | _provider = provider; 35 | } 36 | 37 | public ArchiveContentStream(ArchiveItemInfo archiveFileInfo, FileMode mode, string delimiter, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream) 38 | : base(archiveFileInfo.Open(mode), delimiter, encoding, provider, isRawStream) 39 | { 40 | _provider = provider; 41 | } 42 | 43 | 44 | ~ArchiveContentStream() 45 | { 46 | 47 | } 48 | 49 | } 50 | #endregion ArchiveContentStream 51 | 52 | #endregion StreamContent 53 | 54 | } -------------------------------------------------------------------------------- /src/ps1c/ArchiveItemInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Linq; 7 | using System.Management.Automation; 8 | using System.Management.Automation.Internal; 9 | 10 | namespace PS1C 11 | { 12 | #region ArchiveItemInfo 13 | public class ArchiveItemInfo 14 | { 15 | //Public Extension info 16 | 17 | //public DateTime CreationTime; // {get;set;} 18 | //public DateTime CreationTimeUtc; // {get;set;} 19 | public ArchivePSDriveInfo Drive { 20 | get; 21 | private set; 22 | } 23 | 24 | public DirectoryInfo Directory; // {get;} 25 | 26 | public string DirectoryName 27 | { 28 | get { 29 | if (IsContainer) 30 | { 31 | return Path.GetDirectoryName(PathUtils.TrimEndingDirectorySeparator(FullName)); 32 | } 33 | 34 | return Path.GetDirectoryName(FullName); 35 | } 36 | } 37 | 38 | public bool Exists { 39 | get { 40 | return true; 41 | } 42 | } 43 | 44 | public object Crc32 { 45 | get { 46 | return null; //archiveEntry.Crc32; 47 | } 48 | } 49 | 50 | public string Extension { 51 | get { 52 | return Path.GetExtension(FullName); 53 | } 54 | } 55 | 56 | public string BaseName { 57 | get { 58 | return Path.GetFileNameWithoutExtension(FullName); 59 | } 60 | } 61 | 62 | public string FullName { 63 | get { 64 | return String.Format("{0}:\\{1}", Drive.Name, ArchiveEntry.FullName).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 65 | } 66 | } 67 | 68 | public string FullArchiveName { 69 | get { 70 | return ArchiveEntry.FullName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 71 | } 72 | } 73 | 74 | public bool IsReadOnly 75 | { 76 | get { 77 | return false; 78 | } 79 | set { 80 | 81 | } 82 | } 83 | 84 | //public DateTime LastAccessTime; // {get;set;} 85 | //public DateTime LastAccessTimeUtc; // {get;set;} 86 | public DateTime LastWriteTime 87 | { 88 | get { 89 | return ArchiveEntry.LastWriteTime.DateTime; 90 | } 91 | set { 92 | // Todo: Fix writetime so it updates the archive as well 93 | ArchiveEntry.LastWriteTime = new DateTimeOffset(value); 94 | } 95 | } 96 | 97 | public DateTime LastWriteTimeUtc 98 | { 99 | get { 100 | return this.LastWriteTime.ToUniversalTime(); 101 | } 102 | set { 103 | this.LastWriteTime = value.ToLocalTime(); 104 | } 105 | } 106 | 107 | public long Length { 108 | get { 109 | return ArchiveEntry.Length; 110 | } 111 | } 112 | 113 | public long CompressedLength { 114 | get { 115 | return ArchiveEntry.CompressedLength; 116 | } 117 | } 118 | 119 | public string Name { 120 | get { 121 | if (IsContainer) 122 | { 123 | return Path.GetFileName(PathUtils.TrimEndingDirectorySeparator(ArchiveEntry.FullName)); 124 | } 125 | return ArchiveEntry.Name; 126 | } 127 | } 128 | 129 | internal ZipArchive Archive { 130 | get { 131 | if (ArchiveEntry.Archive.Entries.Count == 0) 132 | { 133 | return null; 134 | } 135 | return ArchiveEntry.Archive; 136 | } 137 | } 138 | 139 | internal ZipArchiveEntry ArchiveEntry { 140 | get; 141 | private set; 142 | 143 | } 144 | 145 | public FileInfo FileSystemContainer { 146 | get { 147 | return new FileInfo(Drive.Root); 148 | } 149 | } 150 | 151 | public bool IsContainer { 152 | get { 153 | return PathUtils.EndsInDirectorySeparator(ArchiveEntry.FullName); 154 | } 155 | } 156 | 157 | public ArchiveItemInfo(ZipArchiveEntry item, ArchivePSDriveInfo drive) 158 | { 159 | Drive = drive; 160 | ArchiveEntry = item; 161 | } 162 | 163 | public ArchiveItemInfo(ArchivePSDriveInfo drive, string path) : this(drive, path, false) 164 | { 165 | 166 | } 167 | 168 | public ArchiveItemInfo(ArchivePSDriveInfo drive, string path, bool createEntry) 169 | { 170 | 171 | if (String.IsNullOrEmpty(path)) 172 | { 173 | throw TraceSource.NewArgumentNullException("path"); 174 | } 175 | 176 | Drive = drive; 177 | 178 | if (path.StartsWith(Drive.Name)) 179 | { 180 | path = PathUtils.GetRelativePath(Drive.Name + ":\\", path); 181 | } 182 | // Path.VolumeSeparatorChar defaults to a / in ubuntu 183 | if (path.Contains( ":" )) 184 | { 185 | throw TraceSource.NewArgumentException(path); 186 | } 187 | 188 | path = path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 189 | 190 | try { 191 | ZipArchive zipArchive = drive.LockArchive(ArchiveProviderStrings.GetChildItemsAction); 192 | ArchiveEntry = zipArchive.GetEntry(path); 193 | 194 | if (ArchiveEntry == null) 195 | { 196 | if (createEntry == true) 197 | { 198 | // Create an entry if not exists 199 | ArchiveEntry = zipArchive.CreateEntry(path); 200 | //ArchiveEntry = zipArchive.GetEntry(path); 201 | 202 | if (ArchiveEntry == null) 203 | { 204 | throw new IOException(ArchiveProviderStrings.PermissionError); 205 | } 206 | } 207 | else 208 | { 209 | string error = String.Format(ArchiveProviderStrings.ItemNotFound, path); 210 | throw new FileNotFoundException(error); 211 | } 212 | 213 | } 214 | 215 | } 216 | catch(Exception e) { 217 | throw e; 218 | } 219 | finally { 220 | drive.UnlockArchive(ArchiveProviderStrings.GetChildItemsAction); 221 | } 222 | 223 | } 224 | 225 | public StreamWriter AppendText() 226 | { 227 | return new StreamWriter( OpenWrite() ); 228 | } 229 | 230 | public void CopyTo(string destFileName) 231 | { 232 | CopyTo(destFileName, false, false); 233 | } 234 | 235 | public void CopyTo(string destFileName, bool overwrite) 236 | { 237 | CopyTo(destFileName, false, overwrite); 238 | } 239 | //Create Method System.IO.FileStream Create() 240 | //CreateObjRef Method System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType) 241 | 242 | public StreamWriter CreateText() 243 | { 244 | return new StreamWriter( OpenWrite() ); 245 | } 246 | 247 | public void Delete() 248 | { 249 | try { 250 | ZipArchive zipArchive = Drive.LockArchive(ArchiveEntry.FullName); 251 | ZipArchiveEntry zipArchiveEntry = zipArchive.GetEntry(ArchiveEntry.FullName); 252 | zipArchiveEntry.Delete(); 253 | } 254 | catch { 255 | 256 | } 257 | finally { 258 | Drive.UnlockArchive(ArchiveEntry.FullName); 259 | } 260 | 261 | } 262 | 263 | public void Decrypt() 264 | { 265 | throw new NotImplementedException(); 266 | } 267 | 268 | public void Encrypt() 269 | { 270 | throw new NotImplementedException(); 271 | } 272 | 273 | 274 | //GetAccessControl Method System.Security.AccessControl.FileSecurity GetAccessControl(), System.Secur... 275 | //GetHashCode Method int GetHashCode() 276 | //GetLifetimeService Method System.Object GetLifetimeService() 277 | //GetObjectData Method void GetObjectData(System.Runtime.Serialization.SerializationInfo info, Sys... 278 | //GetType Method type GetType() 279 | //InitializeLifetimeService Method System.Object InitializeLifetimeService() 280 | 281 | 282 | public void MoveTo(string destFileName) 283 | { 284 | CopyTo(destFileName, true, false); 285 | } 286 | 287 | internal void CopyTo(string destFileName, bool removeItem, bool overwrite) 288 | { 289 | // if (destFileName.Contains(Path.GetInvalidPathChars()) || destFileName.Contains(Path.GetInvalidFileNameChars()) 290 | if (destFileName.IndexOfAny(Path.GetInvalidPathChars()) != -1) 291 | { 292 | throw new InvalidDataException("Path contains invalid characters"); 293 | } 294 | 295 | // Convert Path to its proper dest path 296 | destFileName = destFileName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 297 | // If the destination file is a folder 298 | // We should move/copy the item to that folder. 299 | // Example: 300 | // Move-Item Provider:\a\b\c\file.txt .\d\e\f 301 | // Will move the file to Provider:\d\e\f\file.txt 302 | if (destFileName.EndsWith(Path.AltDirectorySeparatorChar)) 303 | { 304 | destFileName = $"{destFileName}{ArchiveEntry.Name}"; 305 | } 306 | 307 | // Validate if path is filesystem 308 | if (Path.IsPathRooted(destFileName) && !destFileName.StartsWith(Drive.Name)) 309 | { 310 | CopyToFileSystem(destFileName, removeItem, overwrite); 311 | return; 312 | } 313 | 314 | // Cleanup the filesystem path 315 | if (destFileName.StartsWith(Drive.Name)) 316 | { 317 | destFileName = PathUtils.GetRelativePath((Drive.Name + ":\\"), destFileName); 318 | } 319 | else if (destFileName.StartsWith(Drive.Root)) 320 | { 321 | destFileName = PathUtils.GetRelativePath(Drive.Root, destFileName); 322 | } 323 | 324 | CopyToArchive(destFileName, removeItem, overwrite); 325 | } 326 | 327 | internal void CopyToFileSystem(string destFileName, bool removeItem, bool overwrite) 328 | { 329 | if (File.Exists(destFileName) && !overwrite) 330 | { 331 | throw new Exception($"The item exists '{destFileName}'"); 332 | } 333 | 334 | ZipArchive zipArchive = Drive.LockArchive(FullArchiveName); 335 | ZipArchiveEntry thisEntry = zipArchive.GetEntry(ArchiveEntry.FullName); 336 | 337 | thisEntry.ExtractToFile(destFileName); 338 | 339 | if (removeItem) 340 | { 341 | thisEntry.Delete(); 342 | } 343 | 344 | Drive.UnlockArchive(FullArchiveName); 345 | } 346 | 347 | internal void CopyToArchive(string destFileName, bool removeItem, bool overwrite) 348 | { 349 | ZipArchive zipArchive = Drive.LockArchive(FullArchiveName); 350 | 351 | ZipArchiveEntry thisEntry = zipArchive.GetEntry(ArchiveEntry.FullName); 352 | ZipArchiveEntry newEntry = zipArchive.GetEntry(destFileName); 353 | 354 | // Determine if Overwrite is enabled and item exists. 355 | if ((overwrite == false) && (newEntry != null)) 356 | { 357 | throw new Exception($"The item exists '{destFileName}'"); 358 | } 359 | 360 | if (newEntry == null) { 361 | newEntry = zipArchive.CreateEntry(destFileName); 362 | } 363 | 364 | using (Stream thisStream = thisEntry.Open()) 365 | using (Stream newStream = newEntry.Open()) 366 | { 367 | thisStream.CopyTo(newStream); 368 | } 369 | if (removeItem) 370 | { 371 | thisEntry.Delete(); 372 | } 373 | 374 | Drive.UnlockArchive(FullArchiveName, true); 375 | 376 | } 377 | 378 | public ArchiveItemStream Open() 379 | { 380 | return new ArchiveItemStream(this); 381 | } 382 | 383 | public ArchiveItemStream Open(FileMode mode) 384 | { 385 | return new ArchiveItemStream(this); 386 | } 387 | 388 | public ArchiveItemStream Open(FileMode mode, FileAccess access) 389 | { 390 | throw new NotImplementedException(); 391 | } 392 | 393 | public ArchiveItemStream Open(FileMode mode, FileAccess access, FileShare share) 394 | { 395 | throw new NotImplementedException(); 396 | } 397 | 398 | public ArchiveItemStream OpenRead() 399 | { 400 | return Open(); 401 | } 402 | 403 | public StreamReader OpenText() 404 | { 405 | return new StreamReader(Open()); 406 | } 407 | 408 | public ArchiveItemStream OpenWrite() 409 | { 410 | return Open(); 411 | } 412 | 413 | //Refresh Method void Refresh() 414 | //Replace Method System.IO.FileInfo Replace(string destinationFileName, string destinationBa... 415 | //SetAccessControl Method void SetAccessControl(System.Security.AccessControl.FileSecurity fileSecurity) 416 | 417 | public string ReadToEnd() 418 | { 419 | string result; 420 | using (ArchiveItemStream stream = Open(FileMode.Append)) 421 | using (StreamReader streamReader = new StreamReader(stream)) 422 | { 423 | result = streamReader.ReadToEnd(); 424 | } 425 | return result; 426 | } 427 | 428 | internal void ClearContent() 429 | { 430 | ArchiveItemStream fileStream = Open(FileMode.Append); 431 | fileStream.Seek(0, SeekOrigin.Begin); 432 | fileStream.SetLength(0); 433 | fileStream.Flush(); 434 | fileStream.Close(); 435 | fileStream.Dispose(); 436 | } 437 | 438 | } 439 | #endregion ArchiveItemInfo 440 | } -------------------------------------------------------------------------------- /src/ps1c/ArchiveItemStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Management.Automation.Provider; 5 | using System.IO.Compression; 6 | 7 | namespace PS1C 8 | { 9 | #region ArchiveItemStream 10 | public class ArchiveItemStream : System.IO.Stream 11 | { 12 | 13 | private ArchiveItemInfo _itemInfo; 14 | public System.IO.Stream _stream; 15 | 16 | public bool _isClosed; 17 | public override long Length { 18 | get 19 | { 20 | return _stream.Length; 21 | } 22 | } 23 | public override long Position { 24 | get 25 | { 26 | return _stream.Position; 27 | } 28 | set 29 | { 30 | _stream.Position = value; 31 | } 32 | } 33 | 34 | public override bool CanSeek 35 | { 36 | get 37 | { 38 | return _stream.CanSeek; 39 | } 40 | } 41 | public override bool CanRead { 42 | get 43 | { 44 | return _stream.CanRead; 45 | } 46 | } 47 | 48 | public override bool CanWrite { 49 | get 50 | { 51 | return _stream.CanWrite; 52 | } 53 | } 54 | 55 | public override void Flush() 56 | { 57 | _stream.Flush(); 58 | } 59 | public override void SetLength(long value) 60 | { 61 | _stream.SetLength(value); 62 | } 63 | 64 | public override int Read(byte[] buffer, int offset, int count) 65 | { 66 | return _stream.Read(buffer, offset, count); 67 | } 68 | 69 | public override void Write(byte[] buffer, int offset, int count) 70 | { 71 | _stream.Write(buffer, offset, count); 72 | } 73 | 74 | public override long Seek(long offset, SeekOrigin origin) 75 | { 76 | return _stream.Seek(offset, origin); 77 | } 78 | 79 | public ArchiveItemStream(ArchiveItemInfo entry) 80 | { 81 | _itemInfo = entry; 82 | 83 | ZipArchive archive = _itemInfo.Drive.LockArchive(_itemInfo.ArchiveEntry.FullName); 84 | 85 | _stream = archive.GetEntry(_itemInfo.ArchiveEntry.FullName).Open(); 86 | // Sets position to 0 so it can be fresh 87 | _stream.Position = 0; 88 | } 89 | public override void Close() 90 | { 91 | if (!_isClosed) 92 | { 93 | _stream.Flush(); 94 | _stream.Dispose(); 95 | 96 | _itemInfo.Drive.UnlockArchive(_itemInfo.ArchiveEntry.FullName); 97 | 98 | _isClosed = true; 99 | base.Close(); 100 | 101 | GC.Collect(); 102 | } 103 | 104 | } 105 | 106 | public void Dispose() 107 | { 108 | base.Dispose(); 109 | } 110 | 111 | ~ArchiveItemStream() 112 | { 113 | Dispose(); 114 | } 115 | } 116 | #endregion ArchiveItemStream 117 | } -------------------------------------------------------------------------------- /src/ps1c/ArchivePSDriveInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using System.Management.Automation.Provider; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace PS1C 11 | { 12 | public class ArchivePSDriveInfo : PSDriveInfo 13 | { 14 | internal ZipArchive Archive { 15 | get; 16 | private set; 17 | } 18 | private Dictionary _streamsInUse; 19 | 20 | private FileSystemWatcher _fileWatcher; 21 | private int _fileWatcherLock = 0; 22 | 23 | private List _entryCache; 24 | 25 | //internal bool IsStreamInUse() 26 | //internal void OpenStream() 27 | //internal void CloseStream() 28 | 29 | //internal Stream PullStream() // Note this should not be used 30 | 31 | public List _lockedEntries = new List(); 32 | public ZipArchive LockArchive(string entry) 33 | { 34 | if (_lockedEntries.Contains(entry)) 35 | { 36 | throw new Exception("Cannot open file it is already open in another process"); 37 | } 38 | _lockedEntries.Add(entry); 39 | 40 | if (Archive == null) 41 | { 42 | Archive = ZipFile.Open(Root, ZipArchiveMode.Update); 43 | } 44 | 45 | return Archive; 46 | } 47 | 48 | public void UnlockArchive(string entry) 49 | { 50 | UnlockArchive(entry, false); 51 | } 52 | public void UnlockArchive(string entry, bool updateCache) 53 | { 54 | if (!_lockedEntries.Contains(entry)) 55 | { 56 | throw new Exception("Cannot unlock stream it doesnt exist"); 57 | } 58 | 59 | if (!updateCache) 60 | { 61 | _entryCache = null; 62 | } 63 | 64 | _lockedEntries.Remove(entry); 65 | 66 | if (_lockedEntries.Count == 0) 67 | { 68 | Archive.Dispose(); 69 | 70 | Archive = null; 71 | GC.Collect(); 72 | } 73 | } 74 | 75 | internal bool IsStreamInUse() 76 | { 77 | if (Archive != null) 78 | { 79 | return true; 80 | } 81 | return false; 82 | } 83 | public int ActiveHandles { 84 | get { 85 | return _lockedEntries.Count; 86 | } 87 | } 88 | 89 | /// 90 | /// Initializes a new instance of the AccessDBPSDriveInfo class. 91 | /// The constructor takes a single argument. 92 | /// 93 | /// Drive defined by this provider 94 | public ArchivePSDriveInfo(PSDriveInfo driveInfo) : base(driveInfo) 95 | { 96 | UpdateCache(); 97 | } 98 | 99 | 100 | #region ItemCache 101 | 102 | /// 103 | /// Updates the cached entries. 104 | /// 105 | protected private void UpdateCache() 106 | { 107 | try 108 | { 109 | _entryCache = new List(); 110 | ZipArchive zipArchive = LockArchive(ArchiveProviderStrings.GetChildItemsAction); 111 | 112 | foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) 113 | { 114 | _entryCache.Add( new ArchiveItemInfo(zipArchiveEntry, this) ); 115 | } 116 | } 117 | catch(Exception e) 118 | { 119 | throw e; 120 | } 121 | finally 122 | { 123 | UnlockArchive(ArchiveProviderStrings.GetChildItemsAction, true); 124 | } 125 | } 126 | 127 | #endregion ItemCache 128 | 129 | #region ItemHandler 130 | 131 | public IEnumerable GetItem() 132 | { 133 | if (_entryCache == null) 134 | { 135 | UpdateCache(); 136 | } 137 | //UpdateCache(); 138 | foreach (ArchiveItemInfo item in _entryCache) 139 | { 140 | yield return item; 141 | } 142 | } 143 | 144 | public IEnumerable GetItem(string path) 145 | { 146 | IEnumerable results = GetItem(); 147 | 148 | path = PathUtils.TrimEndingDirectorySeparator(path).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 149 | 150 | WildcardPattern wildcardPattern = WildcardPattern.Get(path, WildcardOptions.IgnoreCase | WildcardOptions.Compiled); 151 | 152 | foreach (ArchiveItemInfo item in results) 153 | { 154 | if (wildcardPattern.IsMatch(PathUtils.TrimEndingDirectorySeparator( item.FullArchiveName ))) 155 | { 156 | yield return item; 157 | } 158 | } 159 | } 160 | 161 | public IEnumerable GetItem(string path, bool directory, bool file) 162 | { 163 | IEnumerable results = GetItem(path); 164 | WildcardPattern wildcardPattern = WildcardPattern.Get(path, WildcardOptions.IgnoreCase | WildcardOptions.Compiled); 165 | path = path.TrimStart(Path.AltDirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); 166 | 167 | foreach (ArchiveItemInfo item in results) 168 | { 169 | if ( Path.GetDirectoryName(path) != Path.GetDirectoryName( PathUtils.TrimEndingDirectorySeparator(item.FullArchiveName) ) ) 170 | { 171 | continue; 172 | } 173 | 174 | if ((directory && item.IsContainer) || (file && !item.IsContainer)) 175 | { 176 | yield return item; 177 | } 178 | 179 | } 180 | } 181 | 182 | public bool ItemExists(string path) 183 | { 184 | // Return true if either condition is met. 185 | return ItemExists(path, false) || ItemExists(path, true); 186 | } 187 | 188 | public bool ItemExists(string path, bool directory) 189 | { 190 | path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 191 | 192 | List items = GetItem().ToList(); 193 | 194 | foreach (ArchiveItemInfo i in items) 195 | { 196 | if (!directory && (path == i.FullArchiveName)) 197 | { 198 | return true; 199 | } 200 | 201 | if (directory && PathUtils.EndsInDirectorySeparator(i.FullArchiveName) && (PathUtils.TrimEndingDirectorySeparator(path) == PathUtils.TrimEndingDirectorySeparator(i.FullArchiveName))) 202 | { 203 | return true; 204 | } 205 | } 206 | return false; 207 | } 208 | 209 | public bool IsItemContainer(string path) 210 | { 211 | return ItemExists(path, true); 212 | } 213 | 214 | #endregion ItemHandler 215 | public void buildFolderPaths() 216 | { 217 | 218 | try { 219 | ZipArchive zipArchive = LockArchive(ArchiveProviderStrings.GetChildItemsAction); 220 | 221 | // Generate a list of items to create 222 | List dirList = new List(); 223 | foreach (ZipArchiveEntry entry in zipArchive.Entries) 224 | { 225 | string fullName = entry.FullName; 226 | if (PathUtils.EndsInDirectorySeparator(fullName)) 227 | { 228 | continue; 229 | } 230 | 231 | fullName = Path.GetDirectoryName(fullName) + Path.AltDirectorySeparatorChar; 232 | fullName = fullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 233 | 234 | if (String.IsNullOrEmpty(fullName)) 235 | { 236 | continue; 237 | } 238 | var paths = enumFolderPaths(fullName); 239 | 240 | foreach (string path in paths) 241 | { 242 | if (zipArchive.GetEntry(path) == null) 243 | { 244 | if (!dirList.Contains(path)) 245 | { 246 | dirList.Add(path); 247 | } 248 | } 249 | } 250 | } 251 | 252 | // Generate a list of directories 253 | foreach (string dir in dirList) 254 | { 255 | zipArchive.CreateEntry(dir); 256 | } 257 | 258 | } 259 | catch(Exception e) { 260 | throw e; 261 | } 262 | finally { 263 | UnlockArchive(ArchiveProviderStrings.GetChildItemsAction); 264 | } 265 | } 266 | 267 | private static IEnumerable enumFolderPaths(string path) 268 | { 269 | int i = 0; 270 | while((i = path.IndexOf(Path.AltDirectorySeparatorChar, i+1)) > -1) 271 | { 272 | yield return path.Substring(0, i+1); 273 | } 274 | } 275 | 276 | } 277 | 278 | } -------------------------------------------------------------------------------- /src/ps1c/PS1c.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | net46;net8.0 10 | linux-x64;osx-x64;win; 11 | PS1C 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | All 20 | 21 | 22 | 23 | 24 | 25 | C:/WINDOWS/Microsoft.Net/assembly/GAC_MSIL/System.IO.Compression/v4.0_4.0.0.0__b77a5c561934e089/System.IO.Compression.dll 26 | True 27 | True 28 | 29 | 30 | C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.IO.Compression.FileSystem\v4.0_4.0.0.0__b77a5c561934e089\System.IO.Compression.FileSystem.dll 31 | True 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | PS1C 41 | $(MSBuildProjectDirectory)$(OutputPath) 42 | $(MSBuildStartupDirectory)\$(PSModuleName)\ 43 | $([System.IO.Path]::GetFullPath(`$(MSBuildProjectDirectory)\..\ResGen`)) 44 | $(MSBuildStartupDirectory)\src\$(PSModuleName)\resources 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/ps1c/resources/ArchiveProviderStrings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | GetChildItems 122 | 123 | 124 | Path contains invalid characters. 125 | 126 | 127 | Create File 128 | 129 | 130 | Create Directory 131 | 132 | 133 | Destination: {0} 134 | 135 | 136 | Clear Content 137 | 138 | 139 | Item: {0} 140 | 141 | 142 | Could not find item {0}. 143 | 144 | 145 | An object at the specified path {0} does not exist. 146 | 147 | 148 | You do not have sufficient access rights to perform this operation or the item is hidden, system, or read only. 149 | 150 | 151 | The '{0}' and '{1}' parameters cannot be specified in the same command. 152 | 153 | 154 | Encoding not used when '-AsByteStream' specified. 155 | 156 | 157 | A delimiter cannot be specified when reading the stream one byte at a time. 158 | 159 | 160 | 161 | Cannot rename the specified target, because it represents a path or device name. 162 | 163 | 164 | Rename File 165 | 166 | 167 | Item: {0} Destination: {1} 168 | 169 | 170 | 171 | Copy File 172 | 173 | 174 | Item: {0} Destination: {1} 175 | 176 | 177 | Copy Directory 178 | 179 | 180 | 181 | Invoke Item 182 | 183 | 184 | Item: {0} 185 | 186 | -------------------------------------------------------------------------------- /src/ps1c/resources/Exceptions.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Cannot process argument because the value of argument "{0}" is not valid. Change the value of the "{0}" argument and run the operation again. 122 | 123 | 128 | 129 | Cannot process argument because the value of argument "{0}" is null. Change the value of argument "{0}" to a non-null value. 130 | 131 | 132 | 133 | Cannot process argument because the value of argument "{0}" is out of range. Change argument "{0}" to a value that is within range. 134 | 135 | 143 | 144 | Cannot perform operation because operation "{0}" is not supported. 145 | 146 | 211 | 212 | Cannot detect the encoding of the file. The specified encoding {0} is not supported when the content is read in reverse. 213 | 214 | 215 | Cannot proceed with byte encoding. When using byte encoding the content must be of type byte. 216 | 217 | 218 | Unknown encoding {0}; valid values are {1}. 219 | 220 | 221 | Encoding 'UTF-7' is obsolete, please use UTF-8. 222 | 223 | -------------------------------------------------------------------------------- /src/ps1c/resources/PS1C.ArchiveFileInfo.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PS1C.ArchiveItemInfo 6 | 7 | PS1C.ArchiveItemInfo 8 | 9 | 10 | 11 | 12 | 25 13 | 14 | 15 | 8 16 | 17 | 18 | 25 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | LastWriteTime 28 | 29 | 30 | Length 31 | 32 | 33 | Name 34 | 35 | 36 | FullName 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/ps1c/resources/PS1C.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | GUID="eb74e8da-9ae2-482a-a648-e96550fb8733" 3 | Author="Microsoft Corporation" 4 | CompanyName="Microsoft Corporation" 5 | Copyright="© Microsoft Corporation. All rights reserved." 6 | Description='PowerShell module for working with ZIP archives.' 7 | ModuleVersion="1.0.1" 8 | PowerShellVersion="3.0" 9 | DotNetFrameworkVersion = 4.5 10 | CmdletsToExport = @() 11 | AliasesToExport = @() 12 | NestedModules="PS1C.psm1" 13 | HelpInfoURI = 'https://go.microsoft.com/fwlink/?LinkId=393254' 14 | FormatsToProcess = @('PS1C.ArchiveFileInfo.Format.ps1xml') 15 | 16 | } -------------------------------------------------------------------------------- /src/ps1c/resources/PS1C.psm1: -------------------------------------------------------------------------------- 1 | if ($PSEdition -eq 'core') { 2 | Import-Module "$($PSScriptRoot)\Core\PS1C.dll" 3 | } else { 4 | Import-Module "$($PSScriptRoot)\Desktop\PS1C.dll" 5 | } -------------------------------------------------------------------------------- /src/ps1c/utils/Assert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // The define below is only valid for this file. It allows the methods 5 | // defined here to call Diagnostics.Assert when only ASSERTIONS_TRACE is defined 6 | // Any #if DEBUG is pointless (always true) in this file because of this declaration. 7 | // The presence of the define will cause the System.Diagnostics.Debug.Asser calls 8 | // always to be compiled in for this file. What can be compiled out are the calls to 9 | // System.Management.Automation.Diagnostics.Assert in other files when neither DEBUG 10 | // nor ASSERTIONS_TRACE is defined. 11 | #define DEBUG 12 | using System; 13 | using System.Diagnostics; 14 | using System.Text; 15 | 16 | namespace PS1C 17 | { 18 | /// 19 | /// Exception with a full stack trace excluding the last two frames. 20 | /// 21 | internal class AssertException : SystemException 22 | { 23 | /// 24 | /// Calls the base class with message and sets the stack frame. 25 | /// 26 | /// Repassed to the base class. 27 | internal AssertException(string message) : base(message) 28 | { 29 | // 3 will skip the assertion caller, this method and AssertException.StackTrace 30 | StackTrace = Diagnostics.StackTrace(3); 31 | } 32 | 33 | /// 34 | /// Returns the stack trace set in the constructor. 35 | /// 36 | /// the constructor's stackTrace 37 | public override string StackTrace { get; } 38 | } 39 | 40 | /// 41 | /// This class contain the few methods necessary for 42 | /// the basic assertion use. 43 | /// 44 | /// 45 | /// All methods are public and static. 46 | /// The class cannot derive from the sealed System.Diagnostics.Debug 47 | /// The class was also made sealed. 48 | /// 49 | /// 50 | /// 51 | /// Diagnostics.Assert(x >= 0,"A negative x would have caused early return."); 52 | /// 53 | /// 54 | /// 55 | /// 56 | internal sealed class Diagnostics 57 | { 58 | internal static string StackTrace(int framesToSkip) 59 | { 60 | StackTrace trace = new StackTrace(true); 61 | StackFrame[] frames = trace.GetFrames(); 62 | StringBuilder frameString = new StringBuilder(); 63 | int maxFrames = 10; 64 | maxFrames += framesToSkip; 65 | for (int i = framesToSkip; (i < frames.Length) && (i < maxFrames); i++) 66 | { 67 | StackFrame frame = frames[i]; 68 | frameString.Append(frame.ToString()); 69 | } 70 | 71 | return frameString.ToString(); 72 | } 73 | 74 | private static object s_throwInsteadOfAssertLock = 1; 75 | 76 | private static bool s_throwInsteadOfAssert = false; 77 | /// 78 | /// If set to true will prevent the assertion dialog from showing up 79 | /// by throwing an exception instead of calling Debug.Assert. 80 | /// 81 | /// false for dialog, true for exception 82 | internal static bool ThrowInsteadOfAssert 83 | { 84 | get 85 | { 86 | lock (s_throwInsteadOfAssertLock) 87 | { 88 | return s_throwInsteadOfAssert; 89 | } 90 | } 91 | 92 | set 93 | { 94 | lock (s_throwInsteadOfAssertLock) 95 | { 96 | s_throwInsteadOfAssert = value; 97 | } 98 | } 99 | } 100 | 101 | /// 102 | /// This class only has statics, so we shouldn't need to instantiate any object. 103 | /// 104 | private Diagnostics() { } 105 | 106 | /// 107 | /// Basic assertion with logical condition and message. 108 | /// 109 | /// 110 | /// logical condition that should be true for program to proceed 111 | /// 112 | /// 113 | /// Message to explain why condition should always be true 114 | /// 115 | // These two lines are playing havoc with asmmeta. Since only one asmmeta file 116 | // can be checked in at a time if you compile the asmmeta for a fre build then 117 | // the checked can't compile against it since these methods will not exist. If 118 | // you check in the chk asmmeta the fre build will not compile because it is 119 | // not expecting these methods to exist. 120 | [System.Diagnostics.Conditional("DEBUG")] 121 | [System.Diagnostics.Conditional("ASSERTIONS_TRACE")] 122 | #if RESHARPER_ATTRIBUTES 123 | [JetBrains.Annotations.AssertionMethod] 124 | #endif 125 | internal static void Assert( 126 | #if RESHARPER_ATTRIBUTES 127 | [JetBrains.Annotations.AssertionCondition(JetBrains.Annotations.AssertionConditionType.IS_TRUE)] 128 | #endif 129 | bool condition, 130 | string whyThisShouldNeverHappen) 131 | { 132 | Diagnostics.Assert(condition, whyThisShouldNeverHappen, string.Empty); 133 | } 134 | 135 | /// 136 | /// Basic assertion with logical condition, message and detailed message. 137 | /// 138 | /// 139 | /// logical condition that should be true for program to proceed 140 | /// 141 | /// 142 | /// Message to explain why condition should always be true 143 | /// 144 | /// 145 | /// Additional information about the assertion 146 | /// 147 | // These two lines are playing havoc with asmmeta. Since only one asmmeta file 148 | // can be checked in at a time if you compile the asmmeta for a fre build then 149 | // the checked can't compile against it since these methods will not exist. If 150 | // you check in the chk asmmeta the fre build will not compile because it is 151 | // not expecting these methods to exist. 152 | [System.Diagnostics.Conditional("DEBUG")] 153 | [System.Diagnostics.Conditional("ASSERTIONS_TRACE")] 154 | #if RESHARPER_ATTRIBUTES 155 | [JetBrains.Annotations.AssertionMethod] 156 | #endif 157 | internal static void 158 | Assert( 159 | #if RESHARPER_ATTRIBUTES 160 | [JetBrains.Annotations.AssertionCondition(JetBrains.Annotations.AssertionConditionType.IS_TRUE)] 161 | #endif 162 | bool condition, 163 | string whyThisShouldNeverHappen, string detailMessage) 164 | { 165 | // Early out avoids some slower code below (mostly the locking done in ThrowInsteadOfAssert). 166 | if (condition) return; 167 | 168 | #if ASSERTIONS_TRACE 169 | if (!condition) 170 | { 171 | if (Diagnostics.ThrowInsteadOfAssert) 172 | { 173 | string assertionMessage = "ASSERT: " + whyThisShouldNeverHappen + " " + detailMessage + " "; 174 | AssertException e = new AssertException(assertionMessage); 175 | tracer.TraceException(e); 176 | throw e; 177 | } 178 | 179 | StringBuilder builder = new StringBuilder(); 180 | builder.Append("ASSERT: "); 181 | builder.Append(whyThisShouldNeverHappen); 182 | builder.Append("."); 183 | if (detailMessage.Length != 0) 184 | { 185 | builder.Append(detailMessage); 186 | builder.Append("."); 187 | } 188 | // 2 to skip this method and Diagnostics.StackTace 189 | builder.Append(Diagnostics.StackTrace(2)); 190 | tracer.TraceError(builder.ToString()); 191 | } 192 | #else 193 | if (Diagnostics.ThrowInsteadOfAssert) 194 | { 195 | string assertionMessage = "ASSERT: " + whyThisShouldNeverHappen + " " + detailMessage + " "; 196 | throw new AssertException(assertionMessage); 197 | } 198 | 199 | System.Diagnostics.Debug.Fail(whyThisShouldNeverHappen, detailMessage); 200 | #endif 201 | } 202 | } 203 | } 204 | 205 | -------------------------------------------------------------------------------- /src/ps1c/utils/CorePsPlatform.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | using System.IO; 8 | using System.Runtime.InteropServices; 9 | 10 | using Microsoft.Win32; 11 | //using Microsoft.Win32.Registry; 12 | using Microsoft.Win32.SafeHandles; 13 | 14 | namespace PS1C 15 | { 16 | 17 | /// 18 | /// These are platform abstractions and platform specific implementations. 19 | /// 20 | public static class Platform 21 | { 22 | private static string _tempDirectory = null; 23 | 24 | /// 25 | /// True if the current platform is Linux. 26 | /// 27 | public static bool IsLinux 28 | { 29 | get 30 | { 31 | #if NET6_0_OR_GREATER 32 | return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); 33 | #else 34 | return false; 35 | #endif 36 | } 37 | } 38 | 39 | /// 40 | /// True if the current platform is macOS. 41 | /// 42 | public static bool IsMacOS 43 | { 44 | get 45 | { 46 | #if NET6_0_OR_GREATER 47 | return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); 48 | #else 49 | return false; 50 | #endif 51 | } 52 | } 53 | 54 | /// 55 | /// True if the current platform is Windows. 56 | /// 57 | public static bool IsWindows 58 | { 59 | get 60 | { 61 | #if NET6_0_OR_GREATER 62 | return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 63 | #else 64 | return true; 65 | #endif 66 | } 67 | } 68 | 69 | /// 70 | /// True if the underlying system is NanoServer. 71 | /// 72 | public static bool IsNanoServer 73 | { 74 | get 75 | { 76 | #if UNIX 77 | return false; 78 | #else 79 | if (_isNanoServer.HasValue) { return _isNanoServer.Value; } 80 | 81 | _isNanoServer = false; 82 | using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels")) 83 | { 84 | if (regKey != null) 85 | { 86 | object value = regKey.GetValue("NanoServer"); 87 | if (value != null && regKey.GetValueKind("NanoServer") == RegistryValueKind.DWord) 88 | { 89 | _isNanoServer = (int)value == 1; 90 | } 91 | } 92 | } 93 | 94 | return _isNanoServer.Value; 95 | #endif 96 | } 97 | } 98 | 99 | /// 100 | /// True if the underlying system is IoT. 101 | /// 102 | public static bool IsIoT 103 | { 104 | get 105 | { 106 | #if UNIX 107 | return false; 108 | #else 109 | if (_isIoT.HasValue) { return _isIoT.Value; } 110 | 111 | _isIoT = false; 112 | using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion")) 113 | { 114 | if (regKey != null) 115 | { 116 | object value = regKey.GetValue("ProductName"); 117 | if (value != null && regKey.GetValueKind("ProductName") == RegistryValueKind.String) 118 | { 119 | _isIoT = string.Equals("IoTUAP", (string)value, StringComparison.OrdinalIgnoreCase); 120 | } 121 | } 122 | } 123 | 124 | return _isIoT.Value; 125 | #endif 126 | } 127 | } 128 | 129 | /// 130 | /// True if underlying system is Windows Desktop. 131 | /// 132 | public static bool IsWindowsDesktop 133 | { 134 | get 135 | { 136 | #if UNIX 137 | return false; 138 | #else 139 | if (_isWindowsDesktop.HasValue) { return _isWindowsDesktop.Value; } 140 | 141 | _isWindowsDesktop = !IsNanoServer && !IsIoT; 142 | return _isWindowsDesktop.Value; 143 | #endif 144 | } 145 | } 146 | 147 | #if UNIX 148 | // Gets the location for cache and config folders. 149 | internal static readonly string CacheDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE); 150 | internal static readonly string ConfigDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CONFIG); 151 | #else 152 | // Gets the location for cache and config folders. 153 | internal static readonly string CacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell"; 154 | internal static readonly string ConfigDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + @"\PowerShell"; 155 | 156 | private static bool? _isNanoServer = null; 157 | private static bool? _isIoT = null; 158 | private static bool? _isWindowsDesktop = null; 159 | #endif 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /src/ps1c/utils/CrossCompilerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using PS1C; 4 | // This is used to add or extend class objects with additional functionality to bring parity with newer .net frameworks 5 | 6 | 7 | 8 | namespace PS1C 9 | { 10 | public static class CrossFrameworkExtensions 11 | { 12 | public static char[] Slice(this char[] source, int start, int length) 13 | { 14 | char[] result = new char[length]; 15 | Array.Copy(source, start, result, 0, length); 16 | return result; 17 | } 18 | 19 | public static bool IsNullOrWhiteSpace(this String value) 20 | { 21 | return string.IsNullOrWhiteSpace(value); 22 | } 23 | 24 | public static bool Contains (this String source, string value, StringComparison comparisonType) 25 | { 26 | if (source == null) 27 | { 28 | return false; 29 | } 30 | return source.IndexOf(value, comparisonType) >= 0; 31 | } 32 | 33 | public static bool Contains (this String source, char value, StringComparison comparisonType) 34 | { 35 | if (source == null) 36 | { 37 | return false; 38 | } 39 | return source.IndexOf(value) >= 0; 40 | } 41 | 42 | public static bool Contains (this String source, char[] value) 43 | { 44 | if (source == null) 45 | { 46 | return false; 47 | } 48 | return source.IndexOf(value.ToString()) >= 0; 49 | } 50 | 51 | public static bool EndsWith (this String source, char value) 52 | { 53 | if (source == null || source.Length == 0) 54 | { 55 | return false; 56 | } 57 | return source[source.Length - 1] == value; 58 | } 59 | 60 | } 61 | public static class PathUtils 62 | { 63 | public static bool EndsInDirectorySeparator(string path) 64 | { 65 | if (path.EndsWith(Path.AltDirectorySeparatorChar)) 66 | return true; 67 | if (path.EndsWith(Path.DirectorySeparatorChar)) 68 | return true; 69 | return false; 70 | } 71 | public static string TrimEndingDirectorySeparator(string path) 72 | { 73 | path = path.TrimEnd(Path.DirectorySeparatorChar).TrimEnd(Path.AltDirectorySeparatorChar); 74 | return path; 75 | } 76 | 77 | public static string GetRelativePath(string relativeTo, string path) 78 | { 79 | if (string.IsNullOrEmpty(relativeTo)) 80 | { 81 | throw new ArgumentNullException("relativeTo", "The path is empty. (Parameter 'relativeTo')"); 82 | } 83 | 84 | if (string.IsNullOrEmpty(path)) 85 | { 86 | throw new ArgumentNullException("path", "The path is empty. (Parameter 'path')"); 87 | } 88 | 89 | string p = Path.GetFullPath(path); 90 | string r = Path.GetFullPath(relativeTo); 91 | 92 | if (p == r) 93 | { 94 | return "."; 95 | } 96 | 97 | if (p.StartsWith(r)) 98 | { 99 | return p.Substring(r.Length); 100 | } 101 | 102 | p = p.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 103 | 104 | // Remove leading / if it exists 105 | if (p.StartsWith("\\") || p.StartsWith("/")) 106 | { 107 | p = p.Substring(1, p.Length - 1); 108 | } 109 | 110 | // Should return a ../.. for each directory in the relative path 111 | string[] pParts = p.Split(Path.DirectorySeparatorChar); 112 | 113 | for (int i = 0; i < pParts.Length; i++) 114 | { 115 | pParts[i] = ".."; 116 | } 117 | 118 | return String.Join(Path.DirectorySeparatorChar.ToString(), pParts); 119 | } 120 | 121 | public static string GetFullPath(string path, string basePath) 122 | { 123 | return Path.GetFullPath(Path.Combine(basePath, path)); 124 | } 125 | 126 | public static string Join(string path1, string path2) 127 | { 128 | return Path.Combine(path1, path2); 129 | } 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/ps1c/utils/EncodingUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Text; 8 | using System.Management.Automation; 9 | using System.Management.Automation.Internal; 10 | 11 | namespace PS1C 12 | { 13 | internal static class EncodingConversion 14 | { 15 | internal const string Unknown = "unknown"; 16 | internal const string String = "string"; 17 | internal const string Unicode = "unicode"; 18 | internal const string BigEndianUnicode = "bigendianunicode"; 19 | internal const string Ascii = "ascii"; 20 | internal const string Utf8 = "utf8"; 21 | internal const string Utf8NoBom = "utf8NoBOM"; 22 | internal const string Utf8Bom = "utf8BOM"; 23 | // internal const string Utf7 = "utf7"; // UTF7 is obsolete and was removed 24 | internal const string Utf32 = "utf32"; 25 | internal const string Default = "default"; 26 | internal const string OEM = "oem"; 27 | internal static readonly string[] TabCompletionResults = { 28 | Ascii, 29 | BigEndianUnicode, 30 | OEM, 31 | Unicode, 32 | // Utf7, // UTF7 is obsolete and was removed 33 | Utf8, 34 | Utf8Bom, 35 | Utf8NoBom, 36 | Utf32 37 | }; 38 | 39 | internal static Dictionary encodingMap = new Dictionary(StringComparer.OrdinalIgnoreCase) 40 | { 41 | { Ascii, System.Text.Encoding.ASCII }, 42 | { BigEndianUnicode, System.Text.Encoding.BigEndianUnicode }, 43 | { Default, System.Text.Encoding.Default }, 44 | { OEM, System.Text.Encoding.Default }, 45 | { Unicode, System.Text.Encoding.Unicode }, 46 | //{ Utf7, System.Text.Encoding.UTF7 }, // UTF7 is obsolete and was removed 47 | { Utf8, System.Text.Encoding.UTF8 }, 48 | { Utf8Bom, System.Text.Encoding.UTF8 }, 49 | { Utf8NoBom, System.Text.Encoding.UTF8 }, 50 | { Utf32, System.Text.Encoding.UTF32 }, 51 | { String, System.Text.Encoding.Unicode }, 52 | { Unknown, System.Text.Encoding.Unicode }, 53 | }; 54 | 55 | /// 56 | /// Retrieve the encoding parameter from the command line 57 | /// it throws if the encoding does not match the known ones. 58 | /// 59 | /// A System.Text.Encoding object (null if no encoding specified). 60 | internal static Encoding Convert(Cmdlet cmdlet, string encoding) 61 | { 62 | if (string.IsNullOrEmpty(encoding)) 63 | { 64 | // no parameter passed, default to UTF8 65 | return new UTF8Encoding(false); 66 | } 67 | 68 | Encoding foundEncoding; 69 | if (encodingMap.TryGetValue(encoding, out foundEncoding)) 70 | { 71 | return foundEncoding; 72 | } 73 | 74 | 75 | // error condition: unknown encoding value 76 | string validEncodingValues = string.Join(", ", TabCompletionResults); 77 | string msg = String.Format(Exceptions.OutFile_WriteToFileEncodingUnknown, encoding, validEncodingValues); 78 | 79 | ErrorRecord errorRecord = new ErrorRecord( 80 | TraceSource.NewArgumentException("Encoding"), 81 | "WriteToFileEncodingUnknown", 82 | ErrorCategory.InvalidArgument, 83 | null); 84 | 85 | errorRecord.ErrorDetails = new ErrorDetails(msg); 86 | cmdlet.ThrowTerminatingError(errorRecord); 87 | 88 | return null; 89 | } 90 | } 91 | 92 | /// 93 | /// To make it easier to specify -Encoding parameter, we add an ArgumentTransformationAttribute here. 94 | /// When the input data is of type string and is valid to be converted to System.Text.Encoding, we do 95 | /// the conversion and return the converted value. Otherwise, we just return the input data. 96 | /// 97 | internal sealed class ArgumentToEncodingTransformationAttribute : ArgumentTransformationAttribute 98 | { 99 | public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) 100 | { 101 | switch (inputData) 102 | { 103 | case string stringName: 104 | if (EncodingConversion.encodingMap.TryGetValue(stringName, out Encoding foundEncoding)) 105 | { 106 | return foundEncoding; 107 | } 108 | else 109 | { 110 | return System.Text.Encoding.GetEncoding(stringName); 111 | } 112 | case int intName: 113 | return System.Text.Encoding.GetEncoding(intName); 114 | } 115 | 116 | return inputData; 117 | } 118 | } 119 | 120 | /// 121 | /// Provides the set of Encoding values for tab completion of an Encoding parameter. 122 | /// 123 | internal sealed class ArgumentEncodingCompletionsAttribute : ArgumentCompletionsAttribute 124 | { 125 | public ArgumentEncodingCompletionsAttribute() : base( 126 | EncodingConversion.Ascii, 127 | EncodingConversion.BigEndianUnicode, 128 | EncodingConversion.OEM, 129 | EncodingConversion.Unicode, 130 | //EncodingConversion.Utf7, // UTF7 is obsolete and was removed 131 | EncodingConversion.Utf8, 132 | EncodingConversion.Utf8Bom, 133 | EncodingConversion.Utf8NoBom, 134 | EncodingConversion.Utf32 135 | ) 136 | { } 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /src/ps1c/utils/ExtensibleCompletion.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using System.Management.Automation.Language; 10 | using System.Management.Automation; 11 | 12 | 13 | 14 | namespace PS1C 15 | { 16 | /// 17 | /// This attribute is used to specify an argument completions for a parameter of a cmdlet or function 18 | /// based on string array. 19 | /// 20 | /// [Parameter()] 21 | /// [ArgumentCompletions("Option1","Option2","Option3")] 22 | /// public string Noun { get; set; } 23 | /// 24 | /// 25 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 26 | public class ArgumentCompletionsAttribute : Attribute 27 | { 28 | private string[] _completions; 29 | 30 | /// 31 | /// Initializes a new instance of the ArgumentCompletionsAttribute class. 32 | /// 33 | /// List of complete values. 34 | /// For null arguments. 35 | /// For invalid arguments. 36 | public ArgumentCompletionsAttribute(params string[] completions) 37 | { 38 | if (completions == null) 39 | { 40 | throw TraceSource.NewArgumentNullException("completions"); 41 | } 42 | 43 | if (completions.Length == 0) 44 | { 45 | throw TraceSource.NewArgumentOutOfRangeException("completions", completions); 46 | } 47 | 48 | _completions = completions; 49 | } 50 | 51 | /// 52 | /// The function returns completions for arguments. 53 | /// 54 | public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) 55 | { 56 | var wordToCompletePattern = WildcardPattern.Get(string.IsNullOrWhiteSpace(wordToComplete) ? "*" : wordToComplete + "*", WildcardOptions.IgnoreCase); 57 | 58 | foreach (var str in _completions) 59 | { 60 | if (wordToCompletePattern.IsMatch(str)) 61 | { 62 | yield return new CompletionResult(str, str, CompletionResultType.ParameterValue, str); 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/ps1c/utils/PinvokeDllNames.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romero126/PS1C/57cb36c41ead94aff080772f94f42cbf2c95734e/src/ps1c/utils/PinvokeDllNames.cs -------------------------------------------------------------------------------- /src/ps1c/utils/StreamContent.cs: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT License. 4 | 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.IO; 10 | using System.Management.Automation; 11 | using System.Management.Automation.Internal; 12 | using System.Management.Automation.Provider; 13 | using System.Runtime.CompilerServices; 14 | using System.Runtime.InteropServices; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | 18 | using Dbg = System.Management.Automation; 19 | 20 | namespace PS1C 21 | { 22 | /// 23 | /// The content stream base class for the Stream provider. It Implements both 24 | /// the IContentReader and IContentWriter interfaces. 25 | /// 26 | /// 27 | /// Note, this class does no specific error handling. All errors are allowed to 28 | /// propagate to the caller so that they can be written to the error pipeline 29 | /// if necessary. 30 | /// 31 | public class StreamContentReaderWriter : IContentReader, IContentWriter 32 | { 33 | private Encoding _encoding; 34 | private CmdletProvider _provider; 35 | private Stream _stream; 36 | private StreamReader _reader; 37 | private StreamWriter _writer; 38 | private bool _usingByteEncoding; 39 | private const char DefaultDelimiter = '\n'; 40 | private string _delimiter = $"{DefaultDelimiter}"; 41 | private int[] _offsetDictionary; 42 | private bool _usingDelimiter; 43 | private StringBuilder _currentLineContent; 44 | private bool _isRawStream; 45 | private long _fileOffset; 46 | 47 | // The reader to read stream content backward 48 | private StreamContentBackReader _backReader; 49 | 50 | private bool _alreadyDetectEncoding = false; 51 | 52 | // False to add a newline to the end of the output string, true if not. 53 | private bool _suppressNewline = false; 54 | 55 | /// 56 | /// Constructor for the content stream. 57 | /// 58 | public StreamContentReaderWriter(System.IO.Stream stream, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream) 59 | { 60 | _encoding = encoding; 61 | _usingByteEncoding = usingByteEncoding; 62 | _provider = provider; 63 | _isRawStream = isRawStream; 64 | 65 | CreateStreams(stream, encoding); 66 | } 67 | 68 | /// 69 | /// Constructor for the content stream. 70 | /// 71 | public StreamContentReaderWriter(System.IO.Stream stream, Encoding encoding, bool usingByteEncoding, CmdletProvider provider, bool isRawStream, bool suppressNewline) 72 | : this(stream, encoding, usingByteEncoding, provider, isRawStream) 73 | { 74 | 75 | _suppressNewline = suppressNewline; 76 | } 77 | 78 | /// 79 | /// Constructor for the content stream. 80 | /// 81 | /// 82 | /// The name of the Alternate Data Stream to get the content from. If null or empty, returns 83 | /// the file's primary content. 84 | /// 85 | /// 86 | /// The delimiter to use when reading strings. Each time read is called, all contents up to an including 87 | /// the delimiter is read. 88 | /// 89 | /// 90 | /// The encoding of the file to be read or written. 91 | /// 92 | /// 93 | /// The CmdletProvider invoking this stream 94 | /// 95 | /// 96 | /// Indicates raw stream. 97 | /// 98 | 99 | 100 | public StreamContentReaderWriter( 101 | System.IO.Stream stream, 102 | string delimiter, 103 | Encoding encoding, 104 | CmdletProvider provider, 105 | bool isRawStream) 106 | : this(stream, encoding, false, provider, isRawStream) 107 | { 108 | // If the delimiter is default ('\n') we'll use ReadLine() method. 109 | // Otherwise allocate temporary structures for ReadDelimited() method. 110 | if (!(delimiter.Length == 1 && delimiter[0] == DefaultDelimiter)) 111 | { 112 | _delimiter = delimiter; 113 | _usingDelimiter = true; 114 | 115 | // We expect that we are parsing files where line lengths can be relatively long. 116 | const int DefaultLineLength = 256; 117 | _currentLineContent = new StringBuilder(DefaultLineLength); 118 | 119 | // For Boyer-Moore string search algorithm. 120 | // Populate the offset lookups. 121 | // These will tell us the maximum number of characters 122 | // we can read to generate another possible match (safe shift). 123 | // If we read more characters than this, we risk consuming 124 | // more of the stream than we need. 125 | // 126 | // Because an unicode character size is 2 byte we would to have use 127 | // very large array with 65535 size to keep this safe offsets. 128 | // One solution is to pack unicode character to byte. 129 | // The workaround is to use low byte from unicode character. 130 | // This allow us to use small array with size 256. 131 | // This workaround is the fastest and provides excellent results 132 | // in regular search scenarios when the file contains 133 | // mostly characters from the same alphabet. 134 | _offsetDictionary = new int[256]; 135 | 136 | // If next char from file is not in search pattern safe shift is the search pattern length. 137 | for (var n = 0; n < _offsetDictionary.Length; n++) 138 | { 139 | _offsetDictionary[n] = _delimiter.Length; 140 | } 141 | 142 | // If next char from file is in search pattern we should calculate a safe shift. 143 | char currentChar; 144 | byte lowByte; 145 | for (var i = 0; i < _delimiter.Length; i++) 146 | { 147 | currentChar = _delimiter[i]; 148 | #if NET6_0_OR_GREATER 149 | lowByte = Unsafe.As(ref currentChar); 150 | #else 151 | lowByte = (byte)currentChar; 152 | #endif 153 | 154 | _offsetDictionary[lowByte] = _delimiter.Length - i - 1; 155 | } 156 | } 157 | } 158 | 159 | /// 160 | /// Reads the specified number of characters or a lines from the Stream. 161 | /// 162 | /// 163 | /// If less than 1, then the entire Stream is read at once. If 1 or greater, then 164 | /// readCount is used to determine how many items (ie: lines, bytes, delimited tokens) 165 | /// to read per call. 166 | /// 167 | /// 168 | /// An array of strings representing the character(s) or line(s) read from 169 | /// the Stream. 170 | /// 171 | public IList Read(long readCount) 172 | { 173 | //s_tracer.WriteLine("blocks requested = {0}", readCount); 174 | 175 | ArrayList blocks = new ArrayList(); 176 | bool readToEnd = (readCount <= 0); 177 | bool waitChanges = false; 178 | 179 | if (_alreadyDetectEncoding && _reader.BaseStream.Position == 0) 180 | { 181 | Encoding curEncoding = _reader.CurrentEncoding; 182 | // Close the stream, and reopen the stream to make the BOM correctly processed. 183 | // The reader has already detected encoding, so if we don't reopen the stream, the BOM (if there is any) 184 | // will be treated as a regular character. 185 | // _stream.Dispose(); 186 | CreateStreams(_stream, curEncoding); 187 | _alreadyDetectEncoding = false; 188 | } 189 | 190 | try 191 | { 192 | for (long currentBlock = 0; (currentBlock < readCount) || (readToEnd); ++currentBlock) 193 | { 194 | 195 | if (_usingByteEncoding) 196 | { 197 | if (!ReadByteEncoded(waitChanges, blocks, false)) 198 | break; 199 | } 200 | else 201 | { 202 | if (_usingDelimiter || _isRawStream) 203 | { 204 | if (!ReadDelimited(waitChanges, blocks, false, _delimiter)) 205 | break; 206 | } 207 | else 208 | { 209 | if (!ReadByLine(waitChanges, blocks, false)) 210 | break; 211 | } 212 | } 213 | } 214 | 215 | //s_tracer.WriteLine("blocks read = {0}", blocks.Count); 216 | } 217 | catch (Exception e) 218 | { 219 | if ((e is IOException) || 220 | (e is ArgumentException) || 221 | (e is System.Security.SecurityException) || 222 | (e is UnauthorizedAccessException) || 223 | (e is ArgumentNullException)) 224 | { 225 | // Exception contains specific message about the error occured and so no need for errordetails. 226 | _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, "System.IO.Stream")); 227 | return null; 228 | } 229 | else 230 | throw; 231 | } 232 | 233 | return blocks.ToArray(); 234 | } 235 | 236 | /// 237 | /// Move the pointer of the stream to the position where there are 'backCount' number 238 | /// of items (depends on what we are using: delimiter? line? byts?) to the end of the stream. 239 | /// 240 | /// 241 | internal void SeekItemsBackward(int backCount) 242 | { 243 | if (backCount < 0) 244 | { 245 | // The caller needs to guarantee that 'backCount' is greater or equals to 0 246 | throw TraceSource.NewArgumentException("backCount"); 247 | } 248 | 249 | //s_tracer.WriteLine("blocks seek backwards = {0}", backCount); 250 | 251 | ArrayList blocks = new ArrayList(); 252 | if (_reader != null) 253 | { 254 | // Make the reader automatically detect the encoding 255 | Seek(0, SeekOrigin.Begin); 256 | _reader.Peek(); 257 | _alreadyDetectEncoding = true; 258 | } 259 | 260 | Seek(0, SeekOrigin.End); 261 | 262 | if (backCount == 0) 263 | { 264 | // If backCount is 0, we should move the position to the end of the stream. 265 | // Maybe the "waitForChanges" is true in this case, which means that we are waiting for new inputs. 266 | return; 267 | } 268 | 269 | StringBuilder builder = new StringBuilder(); 270 | foreach (char character in _delimiter) 271 | { 272 | builder.Insert(0, character); 273 | } 274 | 275 | string actualDelimiter = builder.ToString(); 276 | long currentBlock = 0; 277 | string lastDelimiterMatch = null; 278 | 279 | try 280 | { 281 | if (_isRawStream) 282 | { 283 | // We always read to the end for the raw data. 284 | // If it's indicated as RawStream, we move the pointer to the 285 | // beginning of the stream 286 | Seek(0, SeekOrigin.Begin); 287 | return; 288 | } 289 | 290 | for (; currentBlock < backCount; ++currentBlock) 291 | { 292 | if (_usingByteEncoding) 293 | { 294 | if (!ReadByteEncoded(false, blocks, true)) 295 | break; 296 | } 297 | else 298 | { 299 | if (_usingDelimiter) 300 | { 301 | if (!ReadDelimited(false, blocks, true, actualDelimiter)) 302 | break; 303 | // If the delimiter is at the end of the stream, we need to read one more 304 | // to get to the right position. For example: 305 | // ua123ua456ua -- -Tail 1 306 | // If we read backward only once, we get 'ua', and cannot get to the right position 307 | // So we read one more time, get 'ua456ua', and then we can get the right position 308 | lastDelimiterMatch = (string)blocks[0]; 309 | if (currentBlock == 0 && lastDelimiterMatch.Equals(actualDelimiter, StringComparison.Ordinal)) 310 | backCount++; 311 | } 312 | else 313 | { 314 | if (!ReadByLine(false, blocks, true)) 315 | break; 316 | } 317 | } 318 | 319 | blocks.Clear(); 320 | } 321 | 322 | // If usingByteEncoding is true, we don't create the reader and _backReader 323 | if (!_usingByteEncoding) 324 | { 325 | long curStreamPosition = _backReader.GetCurrentPosition(); 326 | if (_usingDelimiter) 327 | { 328 | if (currentBlock == backCount) 329 | { 330 | Diagnostics.Assert(lastDelimiterMatch != null, "lastDelimiterMatch should not be null when currentBlock == backCount"); 331 | if (lastDelimiterMatch.EndsWith(actualDelimiter, StringComparison.Ordinal)) 332 | { 333 | curStreamPosition += _backReader.GetByteCount(_delimiter); 334 | } 335 | } 336 | } 337 | 338 | Seek(curStreamPosition, SeekOrigin.Begin); 339 | } 340 | 341 | //s_tracer.WriteLine("blocks seek position = {0}", _stream.Position); 342 | } 343 | catch (Exception e) 344 | { 345 | if ((e is IOException) || 346 | (e is ArgumentException) || 347 | (e is System.Security.SecurityException) || 348 | (e is UnauthorizedAccessException) || 349 | (e is ArgumentNullException)) 350 | { 351 | // Exception contains specific message about the error occured and so no need for errordetails. 352 | _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, "System.IO.Stream")); 353 | } 354 | else 355 | throw; 356 | } 357 | } 358 | private bool ReadByLine(bool waitChanges, ArrayList blocks, bool readBackward) 359 | { 360 | // Reading lines as strings 361 | string line = readBackward ? _backReader.ReadLine() : _reader.ReadLine(); 362 | 363 | if (line != null) 364 | blocks.Add(line); 365 | 366 | int peekResult = readBackward ? _backReader.Peek() : _reader.Peek(); 367 | if (peekResult == -1) 368 | return false; 369 | else 370 | return true; 371 | } 372 | 373 | private bool ReadDelimited(bool waitChanges, ArrayList blocks, bool readBackward, string actualDelimiter) 374 | { 375 | if (_isRawStream) 376 | { 377 | // when -Raw is used we want to anyway read the whole thing 378 | // so avoiding the while loop by reading the entire content. 379 | string contentRead = _reader.ReadToEnd(); 380 | 381 | if (contentRead.Length > 0) 382 | { 383 | blocks.Add(contentRead); 384 | } 385 | 386 | // We already read whole stream so return EOF. 387 | return false; 388 | } 389 | 390 | 391 | // Since the delimiter is a string, we're essentially 392 | // dealing with a "find the substring" algorithm, but with 393 | // the additional restriction that we cannot read past the 394 | // end of the delimiter. If we read past the end of the delimiter, 395 | // then we'll eat up bytes that we need from the stream. 396 | // The solution is a modified Boyer-Moore string search algorithm. 397 | // This version retains the sub-linear search performance (via the 398 | // lookup tables). 399 | int numRead = 0; 400 | int currentOffset = actualDelimiter.Length; 401 | 402 | #if NET6_0_OR_GREATER 403 | Span readBuffer = stackalloc char[currentOffset]; 404 | #else 405 | char[] readBuffer = new char[currentOffset]; 406 | #endif 407 | 408 | bool delimiterNotFound = true; 409 | _currentLineContent.Clear(); 410 | 411 | do 412 | { 413 | 414 | #if NET6_0_OR_GREATER 415 | // Read in the required batch of characters 416 | numRead = readBackward 417 | ? _backReader.Read(readBuffer.Slice(0, currentOffset)) 418 | : _reader.Read(readBuffer.Slice(0, currentOffset)); 419 | #else 420 | // Read in the required batch of characters 421 | numRead = readBackward 422 | ? _backReader.Read(readBuffer, 0, currentOffset) 423 | : _reader.Read(readBuffer, 0, currentOffset); 424 | #endif 425 | 426 | 427 | if (numRead > 0) 428 | { 429 | 430 | _currentLineContent.Append(readBuffer.Slice(0, numRead)); 431 | 432 | // Look up the final character in our offset table. 433 | // If the character doesn't exist in the lookup table, then it's not in 434 | // our search key. That means the match must happen strictly /after/ the 435 | // current position. Because of that, we can feel confident reading in the 436 | // number of characters in the search key, without the risk of reading too many. 437 | var currentChar = _currentLineContent[_currentLineContent.Length - 1]; 438 | currentOffset = _offsetDictionary[currentChar]; 439 | //currentOffset = _offsetDictionary[Unsafe.As(ref currentChar)]; 440 | 441 | // We want to keep reading if delimiter not found and we haven't hit the end of stream 442 | delimiterNotFound = true; 443 | 444 | // If the final letters matched, then we will get an offset of "0". 445 | // In that case, we'll either have a match (and break from the while loop,) 446 | // or we need to move the scan forward one position. 447 | if (currentOffset == 0) 448 | { 449 | currentOffset = 1; 450 | 451 | if (actualDelimiter.Length <= _currentLineContent.Length) 452 | { 453 | delimiterNotFound = false; 454 | int i = 0; 455 | int j = _currentLineContent.Length - actualDelimiter.Length; 456 | for (; i < actualDelimiter.Length; i++, j++) 457 | { 458 | if (actualDelimiter[i] != _currentLineContent[j]) 459 | { 460 | delimiterNotFound = true; 461 | break; 462 | } 463 | } 464 | } 465 | } 466 | } 467 | 468 | } while (delimiterNotFound && (numRead != 0)); 469 | 470 | // We've reached the end of stream or end of line. 471 | if (_currentLineContent.Length > 0) 472 | { 473 | // Add the block read to the ouptut array list, trimming a trailing delimiter, if present. 474 | // Note: If -Tail was specified, we get here in the course of 2 distinct passes: 475 | // - Once while reading backward simply to determine the appropriate *start position* for later forward reading, ignoring the content of the blocks read (in reverse). 476 | // - Then again during forward reading, for regular output processing; it is only then that trimming the delimiter is necessary. 477 | // (Trimming it during backward reading would not only be unnecessary, but could interfere with determining the correct start position.) 478 | blocks.Add( 479 | !readBackward && !delimiterNotFound 480 | ? _currentLineContent.ToString(0, _currentLineContent.Length - actualDelimiter.Length) 481 | : _currentLineContent.ToString() 482 | ); 483 | } 484 | 485 | int peekResult = readBackward ? _backReader.Peek() : _reader.Peek(); 486 | if (peekResult != -1) 487 | return true; 488 | else 489 | { 490 | if (readBackward && _currentLineContent.Length > 0) 491 | { 492 | return true; 493 | } 494 | 495 | return false; 496 | } 497 | } 498 | private bool ReadByteEncoded(bool waitChanges, ArrayList blocks, bool readBack) 499 | { 500 | if (_isRawStream) 501 | { 502 | // if RawSteam, read all bytes and return. When RawStream is used, we dont 503 | // support -first, -last 504 | byte[] bytes = new byte[_stream.Length]; 505 | int numBytesToRead = (int)_stream.Length; 506 | int numBytesRead = 0; 507 | while (numBytesToRead > 0) 508 | { 509 | // Read may return anything from 0 to numBytesToRead. 510 | int n = _stream.Read(bytes, numBytesRead, numBytesToRead); 511 | 512 | // Break when the end of the stream is reached. 513 | if (n == 0) 514 | break; 515 | 516 | numBytesRead += n; 517 | numBytesToRead -= n; 518 | } 519 | 520 | if (numBytesRead == 0) 521 | { 522 | return false; 523 | } 524 | else 525 | { 526 | blocks.Add(bytes); 527 | return true; 528 | } 529 | } 530 | 531 | if (readBack) 532 | { 533 | if (_stream.Position == 0) 534 | { 535 | return false; 536 | } 537 | 538 | _stream.Position--; 539 | blocks.Add((byte)_stream.ReadByte()); 540 | _stream.Position--; 541 | return true; 542 | } 543 | 544 | // Reading bytes not strings 545 | int byteRead = _stream.ReadByte(); 546 | 547 | // Add the byte we read to the list of blocks 548 | if (byteRead != -1) 549 | { 550 | blocks.Add((byte)byteRead); 551 | return true; 552 | } 553 | else 554 | return false; 555 | } 556 | private void CreateStreams(Stream stream, Encoding encoding) 557 | { 558 | _stream = stream; 559 | 560 | 561 | if (!_usingByteEncoding) 562 | { 563 | // Open the reader stream 564 | _reader = new StreamReader(_stream, encoding); 565 | _backReader = new StreamContentBackReader(_stream, encoding); 566 | 567 | // Open the writer stream 568 | if (_reader != null) 569 | { 570 | _reader.Peek(); 571 | encoding = _reader.CurrentEncoding; 572 | } 573 | 574 | _writer = new StreamWriter(_stream, encoding); 575 | } 576 | } 577 | 578 | /// 579 | /// Moves the current stream position. 580 | /// 581 | /// 582 | /// The offset from the origin to move the position to. 583 | /// 584 | /// 585 | /// The origin from which the offset is calculated. 586 | /// 587 | public void Seek(long offset, SeekOrigin origin) 588 | { 589 | if (_writer != null) { _writer.Flush(); } 590 | 591 | _stream.Seek(offset, origin); 592 | 593 | if (_writer != null) { _writer.Flush(); } 594 | 595 | if (_reader != null) { _reader.DiscardBufferedData(); } 596 | 597 | if (_backReader != null) { _backReader.DiscardBufferedData(); } 598 | } 599 | 600 | public virtual void FinalizeStream() 601 | { 602 | 603 | } 604 | 605 | /// 606 | /// Closes the stream. 607 | /// 608 | public void Close() 609 | { 610 | bool streamClosed = false; 611 | 612 | if (_writer != null) 613 | { 614 | try 615 | { 616 | _writer.Flush(); 617 | _writer.Dispose(); 618 | } 619 | finally 620 | { 621 | streamClosed = true; 622 | } 623 | } 624 | 625 | if (_reader != null) 626 | { 627 | _reader.Dispose(); 628 | streamClosed = true; 629 | } 630 | 631 | if (_backReader != null) 632 | { 633 | _backReader.Dispose(); 634 | streamClosed = true; 635 | } 636 | 637 | if (!streamClosed) 638 | { 639 | _stream.Flush(); 640 | _stream.Dispose(); 641 | } 642 | } 643 | 644 | /// 645 | /// Writes the specified object to the stream. 646 | /// 647 | /// 648 | /// The objects to write to the stream 649 | /// 650 | /// 651 | /// The objects written to the stream. 652 | /// 653 | public IList Write(IList content) 654 | { 655 | 656 | foreach (object line in content) 657 | { 658 | object[] contentArray = line as object[]; 659 | if (contentArray != null) 660 | { 661 | foreach (object obj in contentArray) 662 | { 663 | WriteObject(obj); 664 | } 665 | } 666 | else 667 | { 668 | WriteObject(line); 669 | } 670 | } 671 | 672 | return content; 673 | } 674 | 675 | private void WriteObject(object content) 676 | { 677 | if (content == null) 678 | { 679 | return; 680 | } 681 | 682 | if (_usingByteEncoding) 683 | { 684 | 685 | try 686 | { 687 | byte byteToWrite = (byte)content; 688 | _stream.WriteByte(byteToWrite); 689 | } 690 | catch (InvalidCastException) 691 | { 692 | throw TraceSource.NewArgumentException("content", Exceptions.ByteEncodingError); 693 | } 694 | } 695 | else 696 | { 697 | if (_suppressNewline) 698 | { 699 | _writer.Write(content.ToString()); 700 | } 701 | else 702 | { 703 | _writer.WriteLine(content.ToString()); 704 | } 705 | } 706 | } 707 | 708 | /// 709 | /// Closes the stream. 710 | /// 711 | public void Dispose() 712 | { 713 | Dispose(true); 714 | GC.SuppressFinalize(this); 715 | } 716 | 717 | internal void Dispose(bool isDisposing) 718 | { 719 | if (isDisposing) 720 | { 721 | if (_stream != null) 722 | _stream.Dispose(); 723 | if (_reader != null) 724 | _reader.Dispose(); 725 | if (_backReader != null) 726 | _backReader.Dispose(); 727 | if (_writer != null) 728 | _writer.Dispose(); 729 | } 730 | } 731 | } 732 | 733 | internal sealed class StreamContentBackReader : StreamReader 734 | { 735 | internal StreamContentBackReader(Stream stream, Encoding encoding) 736 | : base(stream, encoding) 737 | { 738 | _stream = stream; 739 | if (_stream.Length > 0) 740 | { 741 | long curPosition = _stream.Position; 742 | _stream.Seek(0, SeekOrigin.Begin); 743 | base.Peek(); 744 | _stream.Position = curPosition; 745 | _currentEncoding = base.CurrentEncoding; 746 | _currentPosition = _stream.Position; 747 | 748 | // Get the oem encoding and system current ANSI code page 749 | _oemEncoding = EncodingConversion.Convert(null, EncodingConversion.OEM); 750 | _defaultAnsiEncoding = EncodingConversion.Convert(null, EncodingConversion.Default); 751 | } 752 | } 753 | 754 | private readonly Stream _stream; 755 | private readonly Encoding _currentEncoding; 756 | private readonly Encoding _oemEncoding; 757 | private readonly Encoding _defaultAnsiEncoding; 758 | 759 | private const int BuffSize = 4096; 760 | private readonly byte[] _byteBuff = new byte[BuffSize]; 761 | private readonly char[] _charBuff = new char[BuffSize]; 762 | private int _byteCount = 0; 763 | private int _charCount = 0; 764 | private long _currentPosition = 0; 765 | private bool? _singleByteCharSet = null; 766 | 767 | private const byte BothTopBitsSet = 0xC0; 768 | private const byte TopBitUnset = 0x80; 769 | 770 | /// 771 | /// If the given encoding is OEM or Default, check to see if the code page 772 | /// is SBCS(single byte character set). 773 | /// 774 | /// 775 | private bool IsSingleByteCharacterSet() 776 | { 777 | if (_singleByteCharSet != null) 778 | return (bool)_singleByteCharSet; 779 | 780 | // Porting note: only UTF-8 is supported on Linux, which is not an SBCS 781 | if ((_currentEncoding.Equals(_oemEncoding) || 782 | _currentEncoding.Equals(_defaultAnsiEncoding)) 783 | && Platform.IsWindows) 784 | { 785 | NativeMethods.CPINFO cpInfo; 786 | if (NativeMethods.GetCPInfo((uint)_currentEncoding.CodePage, out cpInfo) && 787 | cpInfo.MaxCharSize == 1) 788 | { 789 | _singleByteCharSet = true; 790 | return true; 791 | } 792 | } 793 | 794 | _singleByteCharSet = false; 795 | return false; 796 | } 797 | 798 | /// 799 | /// We don't support this method because it is not used by the ReadBackward method in StreamContentReaderWriter. 800 | /// 801 | /// 802 | /// 803 | /// 804 | /// 805 | public override int ReadBlock(char[] buffer, int index, int count) 806 | { 807 | // This method is not supposed to be used 808 | throw TraceSource.NewNotSupportedException(); 809 | } 810 | 811 | /// 812 | /// We don't support this method because it is not used by the ReadBackward method in StreamContentReaderWriter. 813 | /// 814 | /// 815 | public override string ReadToEnd() 816 | { 817 | // This method is not supposed to be used 818 | throw TraceSource.NewNotSupportedException(); 819 | } 820 | 821 | /// 822 | /// Reset the internal character buffer. Use it only when the position of the internal buffer and 823 | /// the base stream do not match. These positions can become mismatch when the user read the data 824 | /// into the buffer and then seek a new position in the underlying stream. 825 | /// 826 | internal new void DiscardBufferedData() 827 | { 828 | base.DiscardBufferedData(); 829 | _currentPosition = _stream.Position; 830 | _charCount = 0; 831 | _byteCount = 0; 832 | } 833 | 834 | /// 835 | /// Return the current actual stream position. 836 | /// 837 | /// 838 | internal long GetCurrentPosition() 839 | { 840 | if (_charCount == 0) 841 | return _currentPosition; 842 | 843 | // _charCount > 0 844 | int byteCount = _currentEncoding.GetByteCount(_charBuff, 0, _charCount); 845 | return (_currentPosition + byteCount); 846 | } 847 | 848 | /// 849 | /// Get the number of bytes the delimiter will 850 | /// be encoded to. 851 | /// 852 | /// 853 | /// 854 | internal int GetByteCount(string delimiter) 855 | { 856 | char[] chars = delimiter.ToCharArray(); 857 | return _currentEncoding.GetByteCount(chars, 0, chars.Length); 858 | } 859 | 860 | /// 861 | /// Peek the next character. 862 | /// 863 | /// Return -1 if we reach the head of the stream. 864 | public override int Peek() 865 | { 866 | if (_charCount == 0) 867 | { 868 | if (RefillCharBuffer() == -1) 869 | { 870 | return -1; 871 | } 872 | } 873 | 874 | // Return the next available character, but DONT consume it (don't advance the _charCount) 875 | return (int)_charBuff[_charCount - 1]; 876 | } 877 | 878 | /// 879 | /// Read the next character. 880 | /// 881 | /// Return -1 if we reach the head of the stream. 882 | public override int Read() 883 | { 884 | if (_charCount == 0) 885 | { 886 | if (RefillCharBuffer() == -1) 887 | { 888 | return -1; 889 | } 890 | } 891 | 892 | _charCount--; 893 | return _charBuff[_charCount]; 894 | } 895 | 896 | /// 897 | /// Read a specific maximum of characters from the current stream into a buffer. 898 | /// 899 | /// Output buffer. 900 | /// Start position to write with. 901 | /// Number of bytes to read. 902 | /// Return the number of characters read, or -1 if we reach the head of the stream. 903 | /// Return the number of characters read, or -1 if we reach the head of the stream. 904 | public override int Read(char[] buffer, int index, int count) 905 | { 906 | #if NET6_0_OR_GREATER 907 | return ReadSpan(new Span(buffer, index, count)); 908 | #else 909 | return ReadSpan(buffer); 910 | #endif 911 | } 912 | 913 | /// 914 | /// Read characters from the current stream into a Span buffer. 915 | /// 916 | /// Output buffer. 917 | /// Return the number of characters read, or -1 if we reach the head of the stream. 918 | 919 | #if NET6_0_OR_GREATER 920 | public override int Read(Span buffer) 921 | { 922 | return ReadSpan(buffer); 923 | } 924 | #else 925 | public int Read(char[] buffer) 926 | { 927 | return ReadSpan(buffer); 928 | } 929 | #endif 930 | 931 | 932 | #if NET6_0_OR_GREATER 933 | private int ReadSpan(Span buffer) 934 | { 935 | // deal with the argument validation 936 | int charRead = 0; 937 | int index = 0; 938 | int count = buffer.Length; 939 | 940 | do 941 | { 942 | if (_charCount == 0) 943 | { 944 | if (RefillCharBuffer() == -1) 945 | { 946 | return charRead; 947 | } 948 | } 949 | 950 | int toRead = _charCount > count ? count : _charCount; 951 | 952 | for (; toRead > 0; toRead--, count--, charRead++) 953 | { 954 | buffer[index++] = _charBuff[--_charCount]; 955 | } 956 | } 957 | while (count > 0); 958 | 959 | return charRead; 960 | } 961 | #else 962 | private int ReadSpan(char[] buffer) 963 | { 964 | // deal with the argument validation 965 | int charRead = 0; 966 | int index = 0; 967 | int count = buffer.Length; 968 | 969 | do 970 | { 971 | if (_charCount == 0) 972 | { 973 | if (RefillCharBuffer() == -1) 974 | { 975 | return charRead; 976 | } 977 | } 978 | 979 | int toRead = _charCount > count ? count : _charCount; 980 | 981 | for (; toRead > 0; toRead--, count--, charRead++) 982 | { 983 | buffer[index++] = _charBuff[--_charCount]; 984 | } 985 | } 986 | while (count > 0); 987 | 988 | return charRead; 989 | } 990 | #endif 991 | 992 | 993 | /// 994 | /// Read a line from the current stream. 995 | /// 996 | /// Return null if we reach the head of the stream. 997 | public override string ReadLine() 998 | { 999 | if (_charCount == 0 && RefillCharBuffer() == -1) 1000 | { 1001 | return null; 1002 | } 1003 | 1004 | int charsToRemove = 0; 1005 | StringBuilder line = new StringBuilder(); 1006 | 1007 | if (_charBuff[_charCount - 1] == '\r' || 1008 | _charBuff[_charCount - 1] == '\n') 1009 | { 1010 | charsToRemove++; 1011 | line.Insert(0, _charBuff[--_charCount]); 1012 | 1013 | if (_charBuff[_charCount] == '\n') 1014 | { 1015 | if (_charCount == 0 && RefillCharBuffer() == -1) 1016 | { 1017 | return string.Empty; 1018 | } 1019 | 1020 | if (_charCount > 0 && _charBuff[_charCount - 1] == '\r') 1021 | { 1022 | charsToRemove++; 1023 | line.Insert(0, _charBuff[--_charCount]); 1024 | } 1025 | } 1026 | } 1027 | 1028 | do 1029 | { 1030 | while (_charCount > 0) 1031 | { 1032 | if (_charBuff[_charCount - 1] == '\r' || 1033 | _charBuff[_charCount - 1] == '\n') 1034 | { 1035 | line.Remove(line.Length - charsToRemove, charsToRemove); 1036 | return line.ToString(); 1037 | } 1038 | else 1039 | { 1040 | line.Insert(0, _charBuff[--_charCount]); 1041 | } 1042 | } 1043 | 1044 | if (RefillCharBuffer() == -1) 1045 | { 1046 | line.Remove(line.Length - charsToRemove, charsToRemove); 1047 | return line.ToString(); 1048 | } 1049 | } while (true); 1050 | } 1051 | 1052 | /// 1053 | /// Refill the internal character buffer. 1054 | /// 1055 | /// 1056 | private int RefillCharBuffer() 1057 | { 1058 | if ((RefillByteBuff()) == -1) 1059 | { 1060 | return -1; 1061 | } 1062 | 1063 | _charCount = _currentEncoding.GetChars(_byteBuff, 0, _byteCount, _charBuff, 0); 1064 | return _charCount; 1065 | } 1066 | 1067 | /// 1068 | /// Refill the internal byte buffer. 1069 | /// 1070 | /// 1071 | private int RefillByteBuff() 1072 | { 1073 | long lengthLeft = _stream.Position; 1074 | 1075 | if (lengthLeft == 0) 1076 | { 1077 | return -1; 1078 | } 1079 | 1080 | int toRead = lengthLeft > BuffSize ? BuffSize : (int)lengthLeft; 1081 | _stream.Seek(-toRead, SeekOrigin.Current); 1082 | 1083 | if (_currentEncoding.Equals(Encoding.UTF8)) 1084 | { 1085 | // It's UTF-8, we need to detect the starting byte of a character 1086 | do 1087 | { 1088 | _currentPosition = _stream.Position; 1089 | byte curByte = (byte)_stream.ReadByte(); 1090 | if ((curByte & BothTopBitsSet) == BothTopBitsSet || 1091 | (curByte & TopBitUnset) == 0x00) 1092 | { 1093 | _byteBuff[0] = curByte; 1094 | _byteCount = 1; 1095 | break; 1096 | } 1097 | } while (lengthLeft > _stream.Position); 1098 | 1099 | if (lengthLeft == _stream.Position) 1100 | { 1101 | // Cannot find a starting byte. The stream is NOT UTF-8 format. Read 'toRead' number of bytes 1102 | _stream.Seek(-toRead, SeekOrigin.Current); 1103 | _byteCount = 0; 1104 | } 1105 | 1106 | _byteCount += _stream.Read(_byteBuff, _byteCount, (int)(lengthLeft - _stream.Position)); 1107 | _stream.Position = _currentPosition; 1108 | } 1109 | else if (_currentEncoding.Equals(Encoding.Unicode) || 1110 | _currentEncoding.Equals(Encoding.BigEndianUnicode) || 1111 | _currentEncoding.Equals(Encoding.UTF32) || 1112 | _currentEncoding.Equals(Encoding.ASCII) || 1113 | IsSingleByteCharacterSet()) 1114 | { 1115 | // Unicode -- two bytes per character 1116 | // BigEndianUnicode -- two types per character 1117 | // UTF-32 -- four bytes per character 1118 | // ASCII -- one byte per character 1119 | // The BufferSize will be a multiple of 4, so we can just read toRead number of bytes 1120 | // if the current stream is encoded by any of these formatting 1121 | 1122 | // If IsSingleByteCharacterSet() returns true, we are sure that the given encoding is OEM 1123 | // or Default, and it is SBCS(single byte character set) code page -- one byte per character 1124 | _currentPosition = _stream.Position; 1125 | _byteCount = _stream.Read(_byteBuff, 0, toRead); 1126 | _stream.Position = _currentPosition; 1127 | } 1128 | else 1129 | { 1130 | // OEM and ANSI code pages include multibyte CJK code pages. If the current code page 1131 | // is MBCS(multibyte character set), we cannot detect a starting byte. 1132 | // UTF-7 has some characters encoded into UTF-16 and then in Modified Base64, 1133 | // the start of these characters is indicated by a '+' sign, and the end is 1134 | // indicated by a character that is not in Modified Base64 set. 1135 | // For these encodings, we cannot detect a starting byte with confidence when 1136 | // reading bytes backward. Throw out exception in these cases. 1137 | string errMsg = String.Format( 1138 | Exceptions.ReadBackward_Encoding_NotSupport, 1139 | _currentEncoding.EncodingName); 1140 | throw new BackReaderEncodingNotSupportedException(errMsg, _currentEncoding.EncodingName); 1141 | } 1142 | 1143 | return _byteCount; 1144 | } 1145 | private static class NativeMethods 1146 | { 1147 | // Default values 1148 | private const int MAX_DEFAULTCHAR = 2; 1149 | private const int MAX_LEADBYTES = 12; 1150 | 1151 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 1152 | internal struct CPINFO 1153 | { 1154 | [MarshalAs(UnmanagedType.U4)] 1155 | internal int MaxCharSize; 1156 | 1157 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_DEFAULTCHAR)] 1158 | public byte[] DefaultChar; 1159 | 1160 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_LEADBYTES)] 1161 | public byte[] LeadBytes; 1162 | }; 1163 | 1164 | /// 1165 | /// Get information on a named code page. 1166 | /// 1167 | /// 1168 | /// 1169 | /// 1170 | [DllImport(PinvokeDllNames.GetCPInfoDllName, CharSet = CharSet.Unicode, SetLastError = true)] 1171 | [return: MarshalAs(UnmanagedType.Bool)] 1172 | internal static extern bool GetCPInfo(uint codePage, out CPINFO lpCpInfo); 1173 | } 1174 | 1175 | /// 1176 | /// The exception that indicates the encoding is not supported when reading backward. 1177 | /// 1178 | internal sealed class BackReaderEncodingNotSupportedException : NotSupportedException 1179 | { 1180 | internal BackReaderEncodingNotSupportedException(string message, string encodingName) 1181 | : base(message) 1182 | { 1183 | EncodingName = encodingName; 1184 | } 1185 | 1186 | internal BackReaderEncodingNotSupportedException(string encodingName) 1187 | { 1188 | EncodingName = encodingName; 1189 | } 1190 | 1191 | /// 1192 | /// Get the encoding name. 1193 | /// 1194 | internal string EncodingName { get; } 1195 | } 1196 | } 1197 | 1198 | } 1199 | 1200 | -------------------------------------------------------------------------------- /src/ps1c/utils/StreamContentDynamicParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | using System.Text; 7 | using System.Management.Automation; 8 | using System.Management.Automation.Provider; 9 | using System.Management.Automation.Runspaces; 10 | using System.Management.Automation.Internal; 11 | using Microsoft.PowerShell.Commands; 12 | 13 | namespace PS1C 14 | { 15 | 16 | /// 17 | /// Defines the dynamic parameters used by both the content reader and writer. 18 | /// 19 | public class StreamContentDynamicParameterBase 20 | { 21 | internal StreamContentDynamicParameterBase() 22 | { 23 | 24 | } 25 | internal StreamContentDynamicParameterBase(CmdletProvider provider) 26 | { 27 | _provider = provider; 28 | } 29 | 30 | internal StreamContentDynamicParameterBase(DriveCmdletProvider provider) 31 | { 32 | _provider = provider; 33 | } 34 | 35 | private readonly CmdletProvider _provider; 36 | 37 | /// 38 | /// Gets or sets the encoding method used when 39 | /// reading data from the file. 40 | /// 41 | [Parameter] 42 | [ArgumentToEncodingTransformationAttribute()] 43 | [ArgumentEncodingCompletionsAttribute] 44 | [ValidateNotNullOrEmpty] 45 | public Encoding Encoding 46 | { 47 | get 48 | { 49 | return _encoding; 50 | } 51 | 52 | set 53 | { 54 | // Check for UTF-7 by checking for code page 65000 55 | // See: https://learn.microsoft.com/dotnet/core/compatibility/corefx#utf-7-code-paths-are-obsolete 56 | if (value != null && value.CodePage == 65000) 57 | { 58 | _provider.WriteWarning(Exceptions.Utf7EncodingObsolete); 59 | } 60 | _encoding = value; 61 | // If an encoding was explicitly set, be sure to capture that. 62 | WasStreamTypeSpecified = true; 63 | } 64 | } 65 | 66 | //private Encoding _encoding = ClrFacade.GetDefaultEncoding(); 67 | private Encoding _encoding = Encoding.Default; 68 | 69 | /// 70 | /// Return file contents as a byte stream or create file from a series of bytes. 71 | /// 72 | [Parameter] 73 | public SwitchParameter AsByteStream { get; set; } 74 | 75 | /// 76 | /// Gets the status of the StreamType parameter. Returns true 77 | /// if the stream was opened with a user-specified encoding, false otherwise. 78 | /// 79 | public bool WasStreamTypeSpecified { get; private set; } 80 | 81 | } 82 | 83 | /// 84 | /// Defines the dynamic parameters used by the set-content and 85 | /// add-content cmdlets. 86 | /// 87 | 88 | public class StreamContentWriterDynamicParameters : StreamContentDynamicParameterBase 89 | { 90 | /// 91 | /// False to add a newline to the end of the output string, true if not. 92 | /// 93 | [Parameter] 94 | public SwitchParameter NoNewline 95 | { 96 | get 97 | { 98 | return _suppressNewline; 99 | } 100 | 101 | set 102 | { 103 | _suppressNewline = value; 104 | } 105 | } 106 | 107 | private bool _suppressNewline = false; 108 | } 109 | 110 | /// 111 | /// Defines the dynamic parameters used by the get-content cmdlet. 112 | /// 113 | public class StreamContentReaderDynamicParameters : StreamContentDynamicParameterBase 114 | { 115 | /// 116 | /// Gets or sets the delimiter to use when reading the file. Custom delimiters 117 | /// may not be used when the file is opened with a "Byte" encoding. 118 | /// 119 | [Parameter] 120 | public string Delimiter 121 | { 122 | get 123 | { 124 | return _delimiter; 125 | } 126 | 127 | set 128 | { 129 | DelimiterSpecified = true; 130 | _delimiter = value; 131 | } 132 | } 133 | 134 | private string _delimiter = "\n"; 135 | 136 | /// 137 | /// When the Raw switch is present, we don't do any breaks on newlines, 138 | /// and only emit one object to the pipeline: all of the content. 139 | /// 140 | [Parameter] 141 | public SwitchParameter Raw 142 | { 143 | get 144 | { 145 | return _isRaw; 146 | } 147 | 148 | set 149 | { 150 | _isRaw = value; 151 | } 152 | } 153 | 154 | private bool _isRaw; 155 | 156 | /// 157 | /// Gets the status of the delimiter parameter. Returns true 158 | /// if the delimiter was explicitly specified by the user, false otherwise. 159 | /// 160 | public bool DelimiterSpecified 161 | { 162 | get; private set; 163 | // get 164 | } 165 | 166 | /// 167 | /// The number of content items to retrieve from the back of the file. 168 | /// 169 | [Parameter(ValueFromPipelineByPropertyName = true)] 170 | // [Alias("Last")] 171 | public int Trail 172 | { 173 | set 174 | { 175 | _backCount = value; 176 | _tailSpecified = true; 177 | } 178 | 179 | get { return _backCount; } 180 | } 181 | 182 | private int _backCount = -1; 183 | private bool _tailSpecified = false; 184 | 185 | } 186 | 187 | /// 188 | /// Defines the dynamic parameters used by the Clear-Content cmdlet. 189 | /// 190 | public class StreamContentClearContentDynamicParameters 191 | { 192 | 193 | } 194 | } -------------------------------------------------------------------------------- /src/ps1c/utils/TraceSource.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | using System; 4 | using System.Diagnostics; 5 | using System.Reflection; 6 | using System.Management.Automation.Internal; 7 | using System.Management.Automation; 8 | 9 | namespace PS1C 10 | { 11 | /// 12 | /// A TraceSource is a representation of a System.Diagnostics.TraceSource instance 13 | /// that is used the the Monad components to produce trace output. 14 | /// 15 | /// 16 | /// It is permitted to subclass 17 | /// but there is no established scenario for doing this, nor has it been tested. 18 | /// 19 | 20 | internal partial class TraceSource 21 | { 22 | 23 | /// 24 | /// Traces the Message and StackTrace properties of the exception 25 | /// and returns the new exception. This is not allowed to call other 26 | /// Throw*Exception variants, since they call this. 27 | /// 28 | /// Exception instance ready to throw. 29 | internal static PSNotSupportedException NewNotSupportedException() 30 | { 31 | string message = String.Format(Exceptions.NotSupported, 32 | new System.Diagnostics.StackTrace().GetFrame(0).ToString()); 33 | var e = new PSNotSupportedException(message); 34 | 35 | return e; 36 | } 37 | 38 | /// 39 | /// Traces the Message and StackTrace properties of the exception 40 | /// and returns the new exception. This is not allowed to call other 41 | /// Throw*Exception variants, since they call this. 42 | /// 43 | /// 44 | /// The name of the parameter whose argument value was null 45 | /// 46 | /// Exception instance ready to throw. 47 | internal static PSArgumentNullException NewArgumentNullException(string paramName) 48 | { 49 | if (string.IsNullOrEmpty(paramName)) 50 | { 51 | throw new ArgumentNullException("paramName"); 52 | } 53 | 54 | string message = String.Format(Exceptions.ArgumentNull, paramName); 55 | var e = new PSArgumentNullException(paramName, message); 56 | 57 | return e; 58 | } 59 | 60 | /// 61 | /// Traces the Message and StackTrace properties of the exception 62 | /// and returns the new exception. This variant allows the caller to 63 | /// specify alternate template text, but only in assembly S.M.A.Core. 64 | /// 65 | /// 66 | /// The name of the parameter whose argument value was invalid 67 | /// 68 | /// 69 | /// The template string for this error 70 | /// 71 | /// 72 | /// Objects corresponding to {0}, {1}, etc. in the resource string 73 | /// 74 | /// Exception instance ready to throw. 75 | internal static PSArgumentNullException NewArgumentNullException( 76 | string paramName, string resourceString, params object[] args) 77 | { 78 | if (string.IsNullOrEmpty(paramName)) 79 | { 80 | throw NewArgumentNullException("paramName"); 81 | } 82 | 83 | if (string.IsNullOrEmpty(resourceString)) 84 | { 85 | throw NewArgumentNullException("resourceString"); 86 | } 87 | 88 | string message = String.Format(resourceString, args); 89 | 90 | // Note that the paramName param comes first 91 | var e = new PSArgumentNullException(paramName, message); 92 | 93 | return e; 94 | } 95 | 96 | /// 97 | /// Traces the Message and StackTrace properties of the exception 98 | /// and returns the new exception. This variant uses the default 99 | /// ArgumentException template text. This is not allowed to call 100 | /// other Throw*Exception variants, since they call this. 101 | /// 102 | /// 103 | /// The name of the parameter whose argument value was invalid 104 | /// 105 | /// Exception instance ready to throw. 106 | internal static PSArgumentException NewArgumentException(string paramName) 107 | { 108 | if (string.IsNullOrEmpty(paramName)) 109 | { 110 | throw new ArgumentNullException("paramName"); 111 | } 112 | 113 | string message = String.Format(Exceptions.Argument, paramName); 114 | // Note that the message param comes first 115 | var e = new PSArgumentException(message, paramName); 116 | 117 | return e; 118 | } 119 | 120 | /// 121 | /// Traces the Message and StackTrace properties of the exception 122 | /// and returns the new exception. This variant allows the caller to 123 | /// specify alternate template text, but only in assembly S.M.A.Core. 124 | /// 125 | /// 126 | /// The name of the parameter whose argument value was invalid 127 | /// 128 | /// 129 | /// The template string for this error 130 | /// 131 | /// 132 | /// Objects corresponding to {0}, {1}, etc. in the resource string 133 | /// 134 | /// Exception instance ready to throw. 135 | internal static PSArgumentException NewArgumentException( 136 | string paramName, string resourceString, params object[] args) 137 | { 138 | if (string.IsNullOrEmpty(paramName)) 139 | { 140 | throw NewArgumentNullException("paramName"); 141 | } 142 | 143 | if (string.IsNullOrEmpty(resourceString)) 144 | { 145 | throw NewArgumentNullException("resourceString"); 146 | } 147 | 148 | string message = String.Format(resourceString, args); 149 | 150 | // Note that the message param comes first 151 | var e = new PSArgumentException(message, paramName); 152 | 153 | return e; 154 | } 155 | /// 156 | /// Traces the Message and StackTrace properties of the exception 157 | /// and returns the new exception. This variant uses the default 158 | /// ArgumentOutOfRangeException template text. This is not allowed to call 159 | /// other Throw*Exception variants, since they call this. 160 | /// 161 | /// 162 | /// The name of the parameter whose argument value was out of range 163 | /// 164 | /// 165 | /// The value of the argument causing the exception 166 | /// 167 | /// Exception instance ready to throw. 168 | internal static PSArgumentOutOfRangeException NewArgumentOutOfRangeException(string paramName, object actualValue) 169 | { 170 | if (string.IsNullOrEmpty(paramName)) 171 | { 172 | throw new ArgumentNullException("paramName"); 173 | } 174 | 175 | string message = String.Format(Exceptions.ArgumentOutOfRange, paramName); 176 | var e = new PSArgumentOutOfRangeException(paramName, actualValue, message); 177 | 178 | return e; 179 | } 180 | 181 | /// 182 | /// Traces the Message and StackTrace properties of the exception 183 | /// and returns the new exception. This variant allows the caller to 184 | /// specify alternate template text, but only in assembly S.M.A.Core. 185 | /// 186 | /// 187 | /// The name of the parameter whose argument value was invalid 188 | /// 189 | /// 190 | /// The value of the argument causing the exception 191 | /// 192 | /// 193 | /// The template string for this error 194 | /// 195 | /// 196 | /// Objects corresponding to {0}, {1}, etc. in the resource string 197 | /// 198 | /// Exception instance ready to throw. 199 | internal static PSArgumentOutOfRangeException NewArgumentOutOfRangeException( 200 | string paramName, object actualValue, string resourceString, params object[] args) 201 | { 202 | if (string.IsNullOrEmpty(paramName)) 203 | { 204 | throw NewArgumentNullException("paramName"); 205 | } 206 | 207 | if (string.IsNullOrEmpty(resourceString)) 208 | { 209 | throw NewArgumentNullException("resourceString"); 210 | } 211 | 212 | string message = String.Format(resourceString, args); 213 | var e = new PSArgumentOutOfRangeException(paramName, actualValue, message); 214 | 215 | return e; 216 | } 217 | 218 | } 219 | 220 | } --------------------------------------------------------------------------------