Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1bf7e3f
Use quit
z------------- Aug 24, 2024
6db16ae
Add std/ in import
z------------- Aug 24, 2024
aeb1507
Un-nimble the directory structure
z------------- Aug 24, 2024
b5e33f2
Raise on invalid input
z------------- Aug 24, 2024
2845c27
Separate tests
z------------- Aug 24, 2024
2253183
Refactor to use jsony-style parse hooks
z------------- Aug 24, 2024
c66a2cd
Add deserialization to object
z------------- Aug 24, 2024
c156197
Separate encoding and decoding
z------------- Aug 24, 2024
884406b
Support decoding to Table
z------------- Aug 24, 2024
d5401be
Support decoding to ref object
z------------- Aug 24, 2024
f1154ce
Support decoding to JsonNode
z------------- Aug 24, 2024
e1dbd37
Require explicit import to use json module
z------------- Aug 24, 2024
131e641
Support nimPreviewSlimSystem
z------------- Aug 24, 2024
1b9cc38
Support strictDefs
z------------- Aug 24, 2024
dbeabe2
Docs
z------------- Aug 24, 2024
c6c5bfb
Rename toBencode to toBencodeObj
z------------- Aug 24, 2024
580a6fe
Add equality and stringify tests
z------------- Aug 24, 2024
940de6c
Various changes
z------------- Aug 24, 2024
0673342
Require Nim 2
z------------- Aug 24, 2024
4b3a99d
Decode to array
z------------- Aug 24, 2024
b66e4c9
BencodeDecodeError type
z------------- Aug 24, 2024
ed110a1
Refactor encoding to use dump hooks
z------------- Aug 24, 2024
1bd780c
Support encoding from object
z------------- Aug 24, 2024
401d646
Add back support for Nim 1.6
z------------- Aug 24, 2024
5efba7b
Support decoding at compile time
z------------- Aug 24, 2024
815d091
Unify impls for Table and object parseHook
z------------- Aug 24, 2024
dca3e4b
Add tool for extracting code blocks from readme
z------------- Aug 24, 2024
f7b0f87
Raise on incomplete dict pair
z------------- Aug 24, 2024
f37828a
Docs
z------------- Aug 24, 2024
39c6c66
Fix incorrect decoding when key missing in object
z------------- Aug 24, 2024
e9039cc
Support encoding from JsonNode
z------------- Aug 24, 2024
f9948a6
Fix encoding nils
z------------- Aug 24, 2024
e608abe
Export InputStream
z------------- Aug 24, 2024
852cdb7
Trigger test on pull requests
z------------- Aug 24, 2024
526c7e1
Update docs
z------------- Aug 24, 2024
b19a7d8
Update tests
z------------- Aug 24, 2024
722b5cd
Allow custom encoded name
z------------- Sep 29, 2024
1e184cd
Use setup-nim-action v2
z------------- Sep 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
branches:
- master
pull_request:
workflow_dispatch:
jobs:
test:
Expand All @@ -20,7 +21,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: true
- uses: jiro4989/setup-nim-action@v1
- uses: jiro4989/setup-nim-action@v2
with:
nim-version: ${{ matrix.nimVersion }}
- run: nimble test -Y --mm:${{ matrix.nimMm }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
*.bin

.vscode

htmldocs
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,61 @@

This is a Nim library to encode/decode [Bencode](https://en.wikipedia.org/wiki/Bencode), the encoding used by the BitTorrent protocol to represent structured data.

## Example
## Examples

```nim
import pkg/bencode

const Expected = "d8:completei20e10:incompletei0e8:intervali1800e12:min intervali900e5:peers6:\x0a\x0a\x0a\x05\x00\x80e"
```

### Custom types

```nim
type
Data = object
interval: int
minInterval {.name: "min interval".}: int
peers: string
complete: int
incomplete: int

let
data = Data(
interval: 1800,
minInterval: 900,
peers: "\x0a\x0a\x0a\x05\x00\x80",
complete: 20,
incomplete: 0,
)
bencodedData = data.toBencode()

doAssert bencodedData == Expected
doAssert Data.fromBencode(bencodedData) == data
```

### BencodeObj and container types

```nim
let
data = be({
"interval": be(1800),
"min interval": be(900),
"peers": be("\x0a\x0a\x0a\x05\x00\x80"),
"complete": be(20),
"incomplete": be(0),
})
bencodedData = data.toBencode

doAssert bencodedData == Expected
doAssert BencodeObj.fromBencode(bencodedData) == data
```

Also works with `std/tables`'s `Table` and `OrderedTable` and `std/json`'s `JsonNode`.

### Old API

```nim
let
data = be({
"interval": be(1800),
Expand All @@ -17,6 +67,6 @@ let
})
bencodedData = bEncode(data)

doAssert bencodedData == "d8:completei20e10:incompletei0e8:intervali1800e12:min intervali900e5:peers6:\x0a\x0a\x0a\x05\x00\x80e"
doAssert bencodedData == Expected
doAssert bDecode(bencodedData) == data
```
23 changes: 12 additions & 11 deletions src/bencode.nim → bencode.nim
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import bencodepkg/[core, json]
import ./bencode/[
decoding,
encoding,
]

export core, json
export decoding, encoding

when isMainModule:
import os

proc die(msg: string; code = 1) {.noReturn.} =
stderr.writeLine(msg)
quit(code)
import std/os
when NimMajor >= 2:
import std/syncio

proc parseFormatArg(arg: string): BencodeFormat =
if arg.len < 2: die("Invalid argument.")
if arg.len < 2: quit("Invalid argument.")
let c = arg[1]
case c
of 'u': Normal
of 'd': Decimal
of 'x': Hexadecimal
else: die("Invalid format argument '" & arg & "'.")
else: quit("Invalid format argument '" & arg & "'.")

let (filename, format) = case paramCount()
of 0:
die("Filename required.")
quit("Filename required.")
of 1:
(paramStr(1), Normal)
else:
block:
let fnIdx = if paramStr(1)[0] == '-': 2 else: 1
(paramStr(fnIdx), paramStr(3 - fnIdx).parseFormatArg)

let
f = open(filename, fmRead)
obj = bDecode(f)
Expand Down
1 change: 0 additions & 1 deletion bencode.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ version = "0.0.7"
author = "z-------------"
description = "Bencode for Nim"
license = "MIT"
srcDir = "src"
bin = @["bencode"]
installExt = @["nim"]

Expand Down
241 changes: 241 additions & 0 deletions bencode/decoding.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import ./private/macros
import ./[
inputstreams,
types,
]
import std/[
parseutils,
strformat,
tables,
]
when NimMajor >= 2:
import std/[
assertions,
syncio,
]

export InputStream
export types
export atEnd, readChar, peekChar, getPosition, readStr # why is this needed?

type
BencodeDecodeErrorKind* = enum
SyntaxError
WrongLength
InvalidValue
BencodeDecodeError* = object of BencodeError
kind* {.requiresInit.}: BencodeDecodeErrorKind
pos* {.requiresInit.}: int

proc newBencodeDecodeError(pos: int; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError =
(ref BencodeDecodeError)(msg: msg, kind: kind, pos: pos)

proc newBencodeDecodeError(s: var InputStream; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError =
newBencodeDecodeError(s.getPosition, kind, msg)

proc consume(s: var InputStream; c: char) =
## Check that the char at the current position is `c`, then consume it.
if s.atEnd:
raise newBencodeDecodeError(s, SyntaxError, &"expected '{c}', got end of input")
let actual = s.readChar()
if actual != c:
raise newBencodeDecodeError(s, SyntaxError, &"expected '{c}', got {actual}")

proc parseInt(str: string; pos: int): int =
result = 0
if parseutils.parseInt(str, result) != str.len:
raise newBencodeDecodeError(pos, SyntaxError, &"invalid integer: {str}")

proc parseHook*(s: var InputStream; v: var string) =
# <length>:<contents>
# get the length
var lengthStr = ""
while not s.atEnd and s.peekChar() != ':':
lengthStr &= s.readChar()
consume(s, ':')
let length = parseInt(lengthStr, s.getPosition)
if length < 0:
raise newBencodeDecodeError(s, InvalidValue, &"invalid string length: {length}")

# read the string
v =
if length >= 0:
try:
s.readStr(length)
except IOError as e:
raise newBencodeDecodeError(s, SyntaxError, e.msg)
else:
""

proc parseHook*(s: var InputStream; v: var int) =
# i<ascii>e
consume(s, 'i')
var iStr = ""
while not s.atEnd and s.peekChar() != 'e':
iStr &= s.readChar()
consume(s, 'e')
v = parseInt(iStr, s.getPosition)

proc parseHook*[T](s: var InputStream; v: var seq[T]) =
# l ... e
v = newSeq[T]()
consume(s, 'l')
while not s.atEnd and s.peekChar() != 'e':
var item = default T
parseHook(s, item)
v.add(item)
consume(s, 'e')

proc parseHook*[T; C: static int](s: var InputStream; v: var array[C, T]) =
# l ... e
v = default array[C, T]
consume(s, 'l')
var i = 0
while not s.atEnd and s.peekChar() != 'e':
if i >= C:
raise newBencodeDecodeError(s, WrongLength, &"list too long: expected {C} items, got at least {i + 1} items")
var item = default T
parseHook(s, item)
v[i] = item
inc i
consume(s, 'e')

template parseHookDictImpl(s: var InputStream; body: untyped) =
# d ... e
var
isReadingKey = true
curKey {.inject.} = ""
consume(s, 'd')
while not s.atEnd and s.peekChar() != 'e':
if isReadingKey:
parseHook(s, curKey)
isReadingKey = false
else:
body
isReadingKey = true
if not isReadingKey:
raise newBencodeDecodeError(s, SyntaxError, "expected value, got end of dict")
consume(s, 'e')

type SomeTable[K, V] = Table[K, V] or OrderedTable[K, V]

proc parseHookTableImpl[T](s: var InputStream; v: var SomeTable[string, T]) =
parseHookDictImpl(s):
var value = default T
parseHook(s, value)
v[curKey] = value

proc parseHook*[T](s: var InputStream; v: var OrderedTable[string, T]) =
# why is this needed?
parseHookTableImpl(s, v)

proc parseHook*[T](s: var InputStream; v: var Table[string, T]) =
parseHookTableImpl(s, v)

proc parseHook*(s: var InputStream; v: var BencodeObj) =
assert not s.atEnd
case s.peekChar()
of 'i':
v = BencodeObj(kind: Int)
parseHook(s, v.i)
of 'l':
v = BencodeObj(kind: List)
parseHook(s, v.l)
of 'd':
v = BencodeObj(kind: Dict)
parseHook(s, v.d)
else:
v = BencodeObj(kind: Str)
parseHook(s, v.s)

import std/json

proc parseHook*(s: var InputStream; v: var JsonNode) =
assert not s.atEnd
case s.peekChar()
of 'i':
var value = default int
parseHook(s, value)
v = newJInt(value)
of 'l':
var value = default seq[JsonNode]
parseHook(s, value)
v = newJArray()
v.elems = value
of 'd':
var value = default OrderedTable[string, JsonNode]
parseHook(s, value)
v = newJObject()
v.fields = value
else:
var value = default string
parseHook(s, value)
v = newJString(value)

proc parseHook*[T: object](s: var InputStream; v: var T) =
parseHookDictImpl(s):
block outer:
for fieldName, fieldValue in fieldPairs(v):
if effectiveName(fieldName, fieldValue) == curKey:
parseHook(s, fieldValue)
break outer
# TODO more efficiently skip to next key?
var b = BencodeObj()
parseHook(s, b)

proc parseHook*[T: ref object](s: var InputStream; v: var T) =
v = T()
parseHook(s, v[])

proc fromBencode(t: typedesc; s: var InputStream): t =
result = default t
parseHook(s, result)

proc fromBencode*(t: typedesc; source: string): t =
## Decode bencoded data from `source` into a value of type `t`.
runnableExamples:
import std/json

type Foo = object
a: int
b: string
c: BencodeObj
d: JsonNode

let data = "d1:b11:hello world1:ai42e1:c7:bencode1:dd5:works17:with JsonNode tooee"
doAssert Foo.fromBencode(data) == Foo(
a: 42,
b: "hello world",
c: Bencode("bencode"),
d: %*{"works": "with JsonNode too"},
)

var s = toInputStream source
fromBencode(t, s)

import std/streams

proc fromBencode*(t: typedesc; s: Stream): t =
## Decode bencoded data from `s` into a value of type `t`.
var s = toInputStream s
fromBencode(t, s)

proc fromBencode*(t: typedesc; f: File): t =
## Decode bencoded data from `f` into a value of type `t`.
fromBencode(t, newFileStream(f))

proc fromBencode*(source: string; t: typedesc): t {.deprecated: "use fromBencode(typedesc, string) instead".} =
## Logically backwards overload to match jsony's interface.
fromBencode(t, source)

proc bDecode*(s: Stream): BencodeObj =
## Same as `BencodeObj.fromBencode(s)<#fromBencode,typedesc,Stream>`_.
fromBencode(BencodeObj, s)

proc bDecode*(source: string): BencodeObj =
## Same as `BencodeObj.fromBencode(source)<#fromBencode,typedesc,string>`_.
fromBencode(BencodeObj, source)

proc bDecode*(f: File): BencodeObj =
## Same as `BencodeObj.fromBencode(f)<#fromBencode,typedesc,File>`_.
fromBencode(BencodeObj, f)
Loading