Last active
December 30, 2024 03:52
-
-
Save Hashbrown777/019d1ee559eb168d23594ae95834bc7b to your computer and use it in GitHub Desktop.
Decode's bencoded data from a file into a powershell structure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#Decode's bencoded data from a file | |
# -Pieces enables reading of piece data, otherwise stops parsing when it sees this list start (to save time) | |
# -Ignore ignores errors such as duplicate key names in dictionaries (new entries are discarded) | |
# -Parse recognised object entries from bittorrent clients are parsed (eg comments & filepaths as utf8, pieces, if enabled, as hexstrings) | |
# -Debug outputs extra info to the console regarding the place in the encoded object we are currently reading for when an error occurs | |
#> | |
Param([switch]$Pieces, [switch]$Ignore, [switch]$Parse, [switch]$Debug) | |
$state = [PSCustomObject]@{ | |
key = $NULL | |
output = $NULL | |
stack = [System.Collections.Stack]::new() | |
} | |
Function Add { Param($what, [switch]$push) | |
if (!$state.stack.Count) { | |
if ($state.output) { | |
throw 'Extra data' | |
} | |
$state.output = $what | |
if (!$push) { | |
$state.stack = $NULL | |
} | |
} | |
elseif ($state.stack.Peek() -is [System.Collections.ArrayList]) { | |
if ($Debug) { ',' | Write-Host -NoNewline } | |
if ($Parse -and ( | |
( | |
$state.stack.Count -eq 5 -and | |
$state.output.info.files.Add -and | |
[Object]::ReferenceEquals($state.output.info.files[-1].path, $state.stack.Peek()) | |
) -or | |
( | |
$state.stack.Count -eq 3 -and | |
[Object]::ReferenceEquals($state.output.info.collections, $state.stack.Peek()) | |
) -or | |
( | |
$state.stack.Count -eq 3 -and | |
$state.output.'announce-list'.Add -and | |
[Object]::ReferenceEquals($state.output.'announce-list'[-1], $state.stack.Peek()) | |
) | |
)) { | |
$what = [System.Text.Encoding]::UTF8.GetString($what) | |
} | |
$state.stack.Peek().Add($what) | Out-Null | |
} | |
elseif (!$state.key) { | |
$state.key = [System.Text.Encoding]::UTF8.GetString($what) | |
if ($Debug) { $state.key | Write-Host -NoNewline } | |
if ((!$Ignore) -and $state.stack.Peek().ContainsKey($state.key)) { | |
throw 'Key already present' | |
} | |
} | |
else { | |
if ($Debug) { ':' | Write-Host -NoNewline } | |
if ($Parse) { | |
switch -Regex ($state.key) { | |
'^(name|announce|created by|comment|source)$' { | |
$what = [System.Text.Encoding]::UTF8.GetString($what) | |
} | |
'^(mtime|md5|crc32|sha1)$' { | |
$what = [System.Text.Encoding]::ASCII.GetString($what) | |
} | |
} | |
} | |
if (!($Ignore -and $state.stack.Peek().ContainsKey($state.key))) { | |
$state.stack.Peek()[$state.key] = $what | |
} | |
$state.key = $NULL | |
} | |
if ($push) { | |
if ($Debug) { '<' | Write-Host -NoNewline } | |
$state.stack.Push($what) | |
} | |
} | |
$reader = [System.IO.BinaryReader]::new( | |
[System.IO.File]::Open( | |
$Input, | |
[System.IO.FileMode]::Open, | |
[System.IO.FileAccess]::Read, | |
[System.IO.FileShare]::ReadWrite | |
), | |
[System.Text.Encoding]::ASCII | |
) | |
Function ReadNumber { Param([char]$until, $char) | |
if (!$char) { | |
$char = $reader.ReadChar() | |
} | |
$negative = $False | |
if ($char -eq '-') { | |
$negative = $True | |
$char = $reader.ReadChar() | |
} | |
$output = 0 | |
while ($char -ne $until) { | |
if ($char -lt [char]'0' -or $char -gt [char]'9') { | |
throw 'Invalid encoding' | |
} | |
$output = $output * 10 + ($char - [char]'0') | |
$char = $reader.ReadChar() | |
} | |
if ($negative) { | |
return -$output | |
} | |
return $output | |
} | |
$width = 20 | |
Filter Hashes { Param($extra) | |
$hash = $NULL | |
Add -push ([System.Collections.ArrayList]::new()) | |
while ($_ -gt 0) { | |
$_ -= $state.output.info['piece length'] | |
$hash = [BitConverter]::ToString($reader.ReadBytes($width)).Replace('-', '') | |
if ($_ -ge 0) { | |
Add $hash | |
} | |
} | |
if ($_) { | |
if (!$extra.Add) { | |
$extra = $state.stack.Peek() | |
} | |
$extra.Add($hash) | Out-Null | |
} | |
$state.stack.Pop() | Out-Null | |
return -$_ | |
} | |
while ( | |
($reader.BaseStream.Position -lt $reader.BaseStream.Length) -and | |
($Pieces -or $state.key -ne 'pieces') | |
) { | |
switch -Exact ($reader.ReadChar()) { | |
'd' { Add -push @{} } | |
'l' { Add -push ([System.Collections.ArrayList]::new()) } | |
'e' { | |
if ($state.key -or !$state.stack.Count) { throw 'Unfinished entry' } | |
if ($Debug) { '>' | Write-Host -NoNewline } | |
$state.stack.Pop() | Out-Null | |
} | |
'i' { | |
Add (ReadNumber 'e') | |
} | |
default { | |
$length = ReadNumber ':' $_ | |
if (!($Parse -and $Pieces -and $state.key -eq 'pieces')) { | |
Add $reader.ReadBytes($length) | |
break | |
} | |
if ($length % $width) { | |
throw 'Piece infohash length invalid' | |
} | |
$length /= $width | |
if (!$state.output.info.files) { | |
$state.output.info.length | Hashes | Out-Null | |
$length -= $state.stack.Peek().pieces.Count | |
} | |
else { | |
Add -push ([System.Collections.ArrayList]::new()) | |
$index = -1 | |
$data = 0 | |
$extra = [System.Collections.ArrayList]::new() | |
$state.output.info.files ` | |
| %{ | |
++$index | |
if ($_.length -lt $data) { | |
$data -= $_.length | |
$state.stack.Peek().Add(@{}) | Out-Null | |
return | |
} | |
Add -push @{} | |
if ($data) { | |
$extra[-1].tail = $index | |
} | |
$state.key = 'start' | |
Add $data | |
$state.key = 'hashes' | |
$data = ($_.length - $data) | Hashes $extra | |
$length -= $state.stack.Peek().hashes.Count | |
$state.key = 'end' | |
if ($data) { | |
--$length | |
Add ($data - $state.output.info['piece length']) | |
$extra[-1] = @{ | |
head = $index | |
tail = $NULL | |
hash = $extra[-1] | |
} | |
} | |
else { | |
Add 0 | |
} | |
$state.stack.Pop() | Out-Null | |
} | |
if ($data) { | |
$extra[-1].tail = ++$index | |
} | |
$extra | %{ Add $_ } | |
$state.stack.Pop() | Out-Null | |
} | |
if ($length) { | |
throw 'Piece infohash length mismatch' | |
} | |
} | |
} | |
} | |
$reader.Close() | |
if ( | |
$state.stack.Count -and | |
!($state.key -eq 'pieces' -and !$Pieces) | |
) { | |
throw 'Unfinished read' | |
} | |
return $state.output |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment