Utils/dd.ps1
|
Register-GnuUtil @{ Name = 'dd' Category = 'File and directory operations' Synopsis = 'Convert and copy a file.' Usage = @('dd [OPERAND]...', 'dd OPTION') Description = @( 'Copy a file, converting and formatting according to the operands.', '', ' bs=BYTES read and write up to BYTES bytes at a time (default: 512); overrides ibs and obs', ' conv=CONVS convert the file as per the comma separated symbol list', ' count=N copy only N input blocks', ' ibs=BYTES read up to BYTES bytes at a time (default: 512)', ' if=FILE read from FILE instead of stdin', ' iflag=FLAGS read as per the comma separated symbol list', ' obs=BYTES write BYTES bytes at a time (default: 512)', ' of=FILE write to FILE instead of stdout', ' oflag=FLAGS write as per the comma separated symbol list', ' seek=N skip N obs-sized blocks at start of output', ' skip=N skip N ibs-sized blocks at start of input', ' status=LEVEL The LEVEL of information to print to stderr; ''none'' suppresses everything but error messages, ''noxfer'' suppresses the final transfer statistics, ''progress'' shows periodic transfer statistics' ) RawArguments = $true OpenErrorFormat = "failed to open '{0}': {1}" Notes = @( 'N and BYTES may be followed by the following multiplicative suffixes:', 'c =1, w =2, b =512, kB =1000, K =1024, MB =1000*1000, M =1024*1024, xM =M,', 'GB =1000*1000*1000, G =1024*1024*1024, and so on for T, P, E, Z, Y.', 'Binary prefixes can be used, too: KiB=K, MiB=M, and so on.', '', 'Each CONV symbol may be:', ' lcase change upper case to lower case', ' ucase change lower case to upper case', ' swab swap every pair of input bytes', ' sync pad every input block with NULs to ibs-size', ' excl fail if the output file already exists', ' nocreat do not create the output file', ' notrunc do not truncate the output file', ' noerror continue after read errors', ' fdatasync physically write output file data before finishing', ' fsync likewise, but also write metadata', '', 'Each FLAG symbol may be:', ' append append mode (makes sense only for output; conv=notrunc suggested)', ' count_bytes treat ''count=N'' as a byte count (iflag only)', ' skip_bytes treat ''skip=N'' as a byte count (iflag only)', ' seek_bytes treat ''seek=N'' as a byte count (oflag only)', ' fullblock accumulate full blocks of input (iflag only)', '', 'PowerShell note: only regular files, the pipeline and the console (no devices such as', '\\.\PhysicalDrive0: one mistake there destroys a disk). The statistics go to the', 'information stream (6). Output to the pipeline is decoded as UTF-8 text lines; use', 'of=FILE for binary data. ascii, ebcdic, block, unblock, sparse and direct I/O flags are', 'not supported. oflag=seek_bytes keeps the first SEEK bytes (GNU 8.32 truncates them).', 'Files are streamed; only pipeline input and output are held in memory.' ) } function script:ConvertFrom-GnuDdNumber { <# .SYNOPSIS A dd number: digits with suffixes c w b kB K MB M ... and products (2x512), $null if invalid, -1 if too large. #> param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) $product = [decimal]1 try { foreach ($part in $Text.Split('x')) { # GNU reads a suffix without digits as one of that unit (bs=M). $spec = $part if ($part -match '^[A-Za-z]') { $spec = '1' + $part } if ($spec -cmatch '^(\d+)([cw])$') { $value = [decimal]$Matches[1] * @{ c = 1; w = 2 }[$Matches[2]] } else { $value = ConvertFrom-GnuSize -Text $spec } if ($null -eq $value) { return $null } $product *= $value } } catch { return [long]-1 } if ($product -gt [long]::MaxValue) { return [long]-1 } [long]$product } function script:Write-GnuDdStatus { <# .SYNOPSIS The transfer statistics, to the information stream. #> param([Parameter(Mandatory)][hashtable]$Stats, [Parameter(Mandatory)][string]$Level, [Parameter(Mandatory)][double]$Seconds) if ($Level -eq 'none') { return } Write-Information -MessageData "$($Stats.FullIn)+$($Stats.PartialIn) records in" -InformationAction Continue Write-Information -MessageData "$($Stats.FullOut)+$($Stats.PartialOut) records out" -InformationAction Continue if ($Level -eq 'noxfer') { return } $invariant = [System.Globalization.CultureInfo]::InvariantCulture $bytes = $Stats.Bytes $text = "$bytes bytes" if ($bytes -eq 1) { $text = '1 byte' } if ($bytes -ge 1000) { $si = (Format-GnuHumanSize -Bytes $bytes -Base 1000 -Nearest) -replace '([kMGTPEZY])$', ' $1B' $iec = (Format-GnuHumanSize -Bytes $bytes -Base 1024 -Nearest) -replace '([KMGTPEZY])$', ' $1iB' # GNU leaves out the IEC form while it has no unit (1000 to 1023 bytes). if ($iec -match '\d$') { $text += " ($si)" } else { $text += " ($si, $iec)" } } # GNU prints the rate with three significant digits: 125 kB/s, 27.0 MB/s, 3.89 GB/s. $rate = 'Infinity B' if ($Seconds -gt 0) { $value = $bytes / $Seconds $unit = 0 while ($value -ge 1000 -and $unit -lt 8) { $value /= 1000; $unit++ } $digits = '0.00' if ($value -ge 100) { $digits = '0' } elseif ($value -ge 10) { $digits = '0.0' } $rate = $value.ToString($digits, $invariant) + ' ' + ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')[$unit] + 'B' } Write-Information -MessageData "$text copied, $($Seconds.ToString('0.######', $invariant)) s, $rate/s" -InformationAction Continue } function script:Read-GnuDdBlock { <# .SYNOPSIS One block of up to Size bytes; shorter only at the end of the stream. #> param([Parameter(Mandatory)][System.IO.Stream]$Stream, [Parameter(Mandatory)][long]$Size) $block = New-Object byte[] $Size $got = 0 do { $count = $Stream.Read($block, $got, $Size - $got) $got += $count } while ($count -gt 0 -and $got -lt $Size) if ($got -lt $Size) { [array]::Resize([ref]$block, $got) } , $block } function script:Write-GnuDdBlock { <# .SYNOPSIS Writes one block (a separate function so tests can make writing fail). #> param([Parameter(Mandatory)][System.IO.Stream]$Stream, [byte[]]$Bytes) $Stream.Write($Bytes, 0, $Bytes.Length) } function script:Convert-GnuDdSwab { <# .SYNOPSIS Swaps byte pairs. Pairs span blocks: like GNU, the last byte of an odd block waits in Carry for the next one. #> param([byte[]]$Block, [Parameter(Mandatory)][AllowEmptyCollection()][System.Collections.Generic.List[byte]]$Carry) $Carry.AddRange($Block) $all = $Carry.ToArray() $Carry.Clear() $even = $all.Length - ($all.Length % 2) for ($k = 0; $k -lt $even; $k += 2) { $swap = $all[$k]; $all[$k] = $all[$k + 1]; $all[$k + 1] = $swap } if ($even -lt $all.Length) { $Carry.Add($all[$even]) [array]::Resize([ref]$all, $even) } , $all } function script:Open-GnuDdOutput { <# .SYNOPSIS Opens of=FILE, positioned and sized like GNU (seek extends or truncates unless notrunc), or reports the error. #> param([Parameter(Mandatory)][hashtable]$Context, [Parameter(Mandatory)][hashtable]$Settings) $name = $Settings.of if ([string]::IsNullOrEmpty($name)) { Write-GnuError -Context $Context -Message "failed to open '': No such file or directory"; return } $path = Resolve-GnuPath -Path $name if ([System.IO.Directory]::Exists($path)) { Write-GnuError -Context $Context -Message "failed to open '$name': Is a directory"; return } $exists = [System.IO.File]::Exists($path) if ($Settings.conv -contains 'excl' -and $exists) { Write-GnuError -Context $Context -Message "failed to open '$name': File exists"; return } if ($Settings.conv -contains 'nocreat' -and -not $exists) { Write-GnuError -Context $Context -Message "failed to open '$name': No such file or directory"; return } $seek = $Settings.seek * $Settings.obs if ($Settings.oflag -contains 'seek_bytes') { $seek = $Settings.seek } $stream = $null try { $stream = New-Object System.IO.FileStream -ArgumentList $path, 'OpenOrCreate', 'Write', 'Read' if ($Settings.oflag -contains 'append') { $seek = $stream.Length } elseif ($Settings.conv -notcontains 'notrunc') { $stream.SetLength($seek) } $stream.Position = $seek $stream } catch { if ($null -ne $stream) { $stream.Dispose() } Write-GnuError -Context $Context -Message "failed to open '$name': $(Get-GnuErrorReason -Exception $_.Exception)" } } function script:Copy-GnuDdData { <# .SYNOPSIS Reads, converts and writes the blocks, filling Stats. A read or write error ends the copy, as in GNU. #> param( [Parameter(Mandatory)][hashtable]$Context, [Parameter(Mandatory)][hashtable]$Settings, [Parameter(Mandatory)][System.IO.Stream]$Source, [Parameter(Mandatory)][System.IO.Stream]$Target, [Parameter(Mandatory)][hashtable]$Stats, [Parameter(Mandatory)][string]$InputName, [Parameter(Mandatory)][string]$OutputName ) $ibs = [long]$Settings.ibs $countBytes = $Settings.iflag -contains 'count_bytes' $offset = $Settings.skip * $ibs if ($Settings.iflag -contains 'skip_bytes') { $offset = $Settings.skip } if ($offset -gt $Source.Length) { Write-GnuWarning -Context $Context -Message "$($InputName): cannot skip to specified offset" $offset = $Source.Length } $Source.Position = $offset $limit = [long]::MaxValue if ($Settings.count -ge 0) { if ($countBytes) { $limit = $Settings.count } else { $limit = $Settings.count * $ibs } } $swab = $Settings.conv -contains 'swab' $ucase = $Settings.conv -contains 'ucase' $lcase = $Settings.conv -contains 'lcase' $carry = New-Object 'System.Collections.Generic.List[byte]' $consumed = [long]0 $action = "error reading '$InputName'" try { while ($Source.Position -lt $Source.Length -and $consumed -lt $limit -and ($Settings.count -lt 0 -or $countBytes -or $Stats.FullIn + $Stats.PartialIn -lt $Settings.count)) { $take = [Math]::Min($ibs, [long]($Source.Length - $Source.Position)) if ($countBytes) { $take = [Math]::Min($take, $limit - $consumed) } $action = "error reading '$InputName'" $block = Read-GnuDdBlock -Stream $Source -Size $take $size = $block.Length $consumed += $size if ($size -eq $ibs) { $Stats.FullIn++ } else { $Stats.PartialIn++ } if ($Settings.conv -contains 'sync' -and $size -lt $ibs) { $padded = New-Object byte[] $ibs [array]::Copy($block, $padded, $size) $block = $padded } if ($ucase -or $lcase) { for ($k = 0; $k -lt $block.Length; $k++) { $b = $block[$k] if ($ucase -and $b -ge 97 -and $b -le 122) { $block[$k] = $b - 32 } if ($lcase -and $b -ge 65 -and $b -le 90) { $block[$k] = $b + 32 } } } if ($Settings.bsGiven) { if ($block.Length -eq $Settings.obs) { $Stats.FullOut++ } else { $Stats.PartialOut++ } } if ($swab) { $block = Convert-GnuDdSwab -Block $block -Carry $carry } $action = "error writing '$OutputName'" Write-GnuDdBlock -Stream $Target -Bytes $block $Stats.Bytes += $block.Length } $action = "error writing '$OutputName'" if ($carry.Count -gt 0) { Write-GnuDdBlock -Stream $Target -Bytes $carry.ToArray() $Stats.Bytes += $carry.Count } $Target.Flush() if ($Target -is [System.IO.FileStream] -and ($Settings.conv -contains 'fsync' -or $Settings.conv -contains 'fdatasync')) { $Target.Flush($true) } } catch { Write-GnuError -Context $Context -Message "$($action): $(Get-GnuErrorReason -Exception $_.Exception)" } if (-not $Settings.bsGiven) { $Stats.FullOut = [long][Math]::Floor($Stats.Bytes / $Settings.obs) if ($Stats.Bytes % $Settings.obs -ne 0) { $Stats.PartialOut = 1 } } } function script:Invoke-GnuDdCopy { <# .SYNOPSIS Implementation of dd. #> param([Parameter(Mandatory)][hashtable]$Context) $settings = @{ if = $null; of = $null; ibs = [long]512; obs = [long]512; count = [long]-1; skip = [long]0; seek = [long]0; conv = @(); iflag = @(); oflag = @(); status = 'default' } $bsGiven = $false foreach ($operand in @($Context.Operands)) { $at = $operand.IndexOf('=') if ($at -le 0) { Write-GnuUsageError -Context $Context -Message "unrecognized operand '$operand'"; return } $key = $operand.Substring(0, $at) $value = $operand.Substring($at + 1) switch -CaseSensitive ($key) { { $_ -eq 'if' -or $_ -eq 'of' } { $settings[$key] = $value } { 'bs', 'ibs', 'obs', 'count', 'skip', 'seek', 'iseek', 'oseek' -ccontains $_ } { $number = ConvertFrom-GnuDdNumber -Text $value if ($null -eq $number -or ($number -eq 0 -and 'bs', 'ibs', 'obs' -ccontains $key)) { Write-GnuError -Context $Context -Message "invalid number: '$value'"; return } if ($number -eq -1) { Write-GnuError -Context $Context -Message "invalid number: '$value': Value too large for defined data type"; return } switch ($key) { 'bs' { $settings.ibs = $number; $settings.obs = $number; $bsGiven = $true } 'iseek' { $settings.skip = $number } 'oseek' { $settings.seek = $number } default { $settings[$key] = $number } } } { 'conv', 'iflag', 'oflag' -ccontains $_ } { $valid = @{ conv = @('lcase', 'ucase', 'swab', 'sync', 'excl', 'nocreat', 'notrunc', 'noerror', 'fdatasync', 'fsync'); iflag = @('count_bytes', 'skip_bytes', 'fullblock'); oflag = @('append', 'seek_bytes') }[$key] foreach ($symbol in $value.Split(',')) { $what = @{ conv = 'conversion'; iflag = 'input flag'; oflag = 'output flag' }[$key] if ($valid -cnotcontains $symbol) { Write-GnuUsageError -Context $Context -Message "invalid $($what): '$symbol'"; return } } $settings[$key] = $value.Split(',') } 'status' { if (@('none', 'noxfer', 'progress') -cnotcontains $value) { Write-GnuUsageError -Context $Context -Message "invalid status level: '$value'"; return } $settings.status = $value } default { Write-GnuUsageError -Context $Context -Message "unrecognized operand '$operand'"; return } } } if ($settings.conv -contains 'lcase' -and $settings.conv -contains 'ucase') { Write-GnuError -Context $Context -Message 'cannot combine lcase and ucase'; return } if ($settings.conv -contains 'excl' -and $settings.conv -contains 'nocreat') { Write-GnuError -Context $Context -Message 'cannot combine excl and nocreat'; return } $settings.bsGiven = $bsGiven $clock = [System.Diagnostics.Stopwatch]::StartNew() $inputName = '-' if ($null -ne $settings.if) { $inputName = $settings.if } if (-not [string]::IsNullOrEmpty($settings.if) -and [System.IO.Directory]::Exists((Resolve-GnuPath -Path $settings.if))) { # GNU opens a directory, fails on the first read, and still creates the output and reports 0+0 records. Write-GnuError -Context $Context -Message "error reading '$($settings.if)': Is a directory" $source = New-Object System.IO.MemoryStream } else { # Other writers are allowed so that if=X of=X behaves like GNU instead of failing with a sharing violation. $source = Open-GnuInputStream -Context $Context -Operand $inputName -AllowWriters if ($null -eq $source) { return } } $stats = @{ FullIn = [long]0; PartialIn = [long]0; FullOut = [long]0; PartialOut = [long]0; Bytes = [long]0 } try { if ($null -eq $settings.of) { $target = New-Object System.IO.MemoryStream } else { $target = Open-GnuDdOutput -Context $Context -Settings $settings if ($null -eq $target) { return } } try { $outputName = 'standard output' if ($null -ne $settings.of) { $outputName = $settings.of } Copy-GnuDdData -Context $Context -Settings $settings -Source $source -Target $target -Stats $stats -InputName $inputName -OutputName $outputName if ($null -eq $settings.of -and $target.Length -gt 0) { ConvertTo-GnuOutputLine -Text $script:GnuUtf8.GetString($target.ToArray()) } } finally { $target.Dispose() } } finally { $source.Dispose() } Write-GnuDdStatus -Stats $stats -Level $settings.status -Seconds $clock.Elapsed.TotalSeconds } |