Skip to content

Instantly share code, notes, and snippets.

@Hashbrown777
Last active December 30, 2024 03:52
Show Gist options
  • Save Hashbrown777/019d1ee559eb168d23594ae95834bc7b to your computer and use it in GitHub Desktop.
Save Hashbrown777/019d1ee559eb168d23594ae95834bc7b to your computer and use it in GitHub Desktop.
Decode's bencoded data from a file into a powershell structure
<#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