From 1bf7e3f0f5e801ad205126e96474385f2de4774f Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 01/38] Use quit --- src/bencode.nim | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/bencode.nim b/src/bencode.nim index 98fd10e..e9ecf84 100644 --- a/src/bencode.nim +++ b/src/bencode.nim @@ -5,22 +5,18 @@ export core, json when isMainModule: import os - proc die(msg: string; code = 1) {.noReturn.} = - stderr.writeLine(msg) - quit(code) - 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: From 6db16aeed5e1617ea1b7d57aa680f7d60569d7ab Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 02/38] Add std/ in import --- src/bencode.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bencode.nim b/src/bencode.nim index e9ecf84..999a477 100644 --- a/src/bencode.nim +++ b/src/bencode.nim @@ -3,7 +3,7 @@ import bencodepkg/[core, json] export core, json when isMainModule: - import os + import std/os proc parseFormatArg(arg: string): BencodeFormat = if arg.len < 2: quit("Invalid argument.") From aeb15070e27fa24caaa9a0b21bf57fdcdc0392b6 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 03/38] Un-nimble the directory structure --- src/bencode.nim => bencode.nim | 2 +- bencode.nimble | 1 - {src/bencodepkg => bencode}/core.nim | 0 {src/bencodepkg => bencode}/json.nim | 0 {src/bencodepkg => bencode}/types.nim | 0 tests/config.nims | 1 - tests/nim.cfg | 1 + 7 files changed, 2 insertions(+), 3 deletions(-) rename src/bencode.nim => bencode.nim (95%) rename {src/bencodepkg => bencode}/core.nim (100%) rename {src/bencodepkg => bencode}/json.nim (100%) rename {src/bencodepkg => bencode}/types.nim (100%) delete mode 100644 tests/config.nims create mode 100644 tests/nim.cfg diff --git a/src/bencode.nim b/bencode.nim similarity index 95% rename from src/bencode.nim rename to bencode.nim index 999a477..80668df 100644 --- a/src/bencode.nim +++ b/bencode.nim @@ -1,4 +1,4 @@ -import bencodepkg/[core, json] +import ./bencode/[core, json] export core, json diff --git a/bencode.nimble b/bencode.nimble index 80f8ee3..16ae322 100644 --- a/bencode.nimble +++ b/bencode.nimble @@ -4,7 +4,6 @@ version = "0.0.7" author = "z-------------" description = "Bencode for Nim" license = "MIT" -srcDir = "src" bin = @["bencode"] installExt = @["nim"] diff --git a/src/bencodepkg/core.nim b/bencode/core.nim similarity index 100% rename from src/bencodepkg/core.nim rename to bencode/core.nim diff --git a/src/bencodepkg/json.nim b/bencode/json.nim similarity index 100% rename from src/bencodepkg/json.nim rename to bencode/json.nim diff --git a/src/bencodepkg/types.nim b/bencode/types.nim similarity index 100% rename from src/bencodepkg/types.nim rename to bencode/types.nim diff --git a/tests/config.nims b/tests/config.nims deleted file mode 100644 index 3bb69f8..0000000 --- a/tests/config.nims +++ /dev/null @@ -1 +0,0 @@ -switch("path", "$projectDir/../src") \ No newline at end of file diff --git a/tests/nim.cfg b/tests/nim.cfg new file mode 100644 index 0000000..aa3c1b9 --- /dev/null +++ b/tests/nim.cfg @@ -0,0 +1 @@ +--path:"$projectDir/.." From b5e33f24027eeb757c44d264fa28f6b3a9452394 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 04/38] Raise on invalid input --- bencode/core.nim | 52 +++++++++++++++++++++++++++++++----------------- tests/test.nim | 37 ++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/bencode/core.nim b/bencode/core.nim index 181c88b..a7292a6 100644 --- a/bencode/core.nim +++ b/bencode/core.nim @@ -1,5 +1,6 @@ import std/[ streams, + strformat, strutils, tables, ] @@ -47,7 +48,15 @@ proc bEncode*(obj: BencodeObj): string = # decode # -proc bDecode*(s: Stream): BencodeObj +proc consume(s: Stream; c: char) = + ## Check that the char at the current position is `c`, then consume it. + if s.atEnd: + raise (ref ValueError)(msg: &"expected '{c}', got end of input") + let actual = s.readChar() + if actual != c: + raise (ref ValueError)(msg: &"expected '{c}', got {actual}") + +proc decode(s: Stream): BencodeObj proc decodeStr(s: Stream): BencodeObj = # : @@ -55,8 +64,10 @@ proc decodeStr(s: Stream): BencodeObj = var lengthStr = "" while not s.atEnd and s.peekChar() != ':': lengthStr &= s.readChar() - discard s.readChar() # advance past the ':' + consume(s, ':') let length = parseInt(lengthStr) + if length < 0: + raise (ref ValueError)(msg: &"invalid string length: {length}") # read the string let str = @@ -64,47 +75,49 @@ proc decodeStr(s: Stream): BencodeObj = s.readStr(length) else: "" + if str.len != length: + raise (ref ValueError)(msg: &"string too short: expected {length} characters, got {str.len} characters") BencodeObj(kind: bkStr, s: str) proc decodeInt(s: Stream): BencodeObj = # ie + consume(s, 'i') var iStr = "" - discard s.readChar() # 'i' while not s.atEnd and s.peekChar() != 'e': iStr &= s.readChar() - discard s.readChar() # 'e' + consume(s, 'e') BencodeObj(kind: bkInt, i: parseInt(iStr)) proc decodeList(s: Stream): BencodeObj = # l ... e - var l: seq[BencodeObj] - discard s.readChar() # advance past the 'l' + var l = newSeq[BencodeObj]() + consume(s, 'l') while not s.atEnd and s.peekChar() != 'e': - l.add(bDecode(s)) - discard s.readChar() # 'e' + l.add(decode(s)) + consume(s, 'e') BencodeObj(kind: bkList, l: l) proc decodeDict(s: Stream): BencodeObj = # d ... e var - d: OrderedTable[string, BencodeObj] + d = initOrderedTable[string, BencodeObj]() isReadingKey = true - curKey: string - discard s.readChar() # 'd' + curKey = "" + consume(s, 'd') while not s.atEnd and s.peekChar() != 'e': if isReadingKey: - let keyObj = bDecode(s) + let keyObj = decode(s) if keyObj.kind != bkStr: - raise newException(ValueError, "invalid dictionary key: expected " & $bkStr & ", got " & $keyObj.kind) + raise newException(ValueError, &"invalid dictionary key: expected {bkStr}, got {keyObj.kind}") curKey = keyObj.s isReadingKey = false else: - d[curKey] = bDecode(s) + d[curKey] = decode(s) isReadingKey = true - discard s.readChar() # 'e' + consume(s, 'e') BencodeObj(kind: bkDict, d: d) -proc bDecode*(s: Stream): BencodeObj = +proc decode(s: Stream): BencodeObj = assert not s.atEnd result = case s.peekChar() of 'i': decodeInt(s) @@ -112,8 +125,11 @@ proc bDecode*(s: Stream): BencodeObj = of 'd': decodeDict(s) else: decodeStr(s) +proc bDecode*(s: Stream): BencodeObj = + decode(s) + proc bDecode*(source: string): BencodeObj = - bDecode(newStringStream(source)) + decode(newStringStream(source)) proc bDecode*(f: File): BencodeObj = - bDecode(newFileStream(f)) + decode(newFileStream(f)) diff --git a/tests/test.nim b/tests/test.nim index 5c64144..a8d38e9 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -1,9 +1,10 @@ import ./utils import pkg/bencode -import std/unittest import std/[ json, + strutils, tables, + unittest, ] test "basic encode/decode": @@ -109,17 +110,37 @@ test "execution terminates for invalid bencode input": discard test "string too short": - const data = "10:hello" - check bDecode(data) == Bencode("hello") + let exception = + expect ValueError: + discard bDecode("10:hello") + check "string too short" in exception.msg test "invalid string length": - const data = "-5:hello" - check bDecode(data) == Bencode("") + let exception = + expect ValueError: + discard bDecode("-5:hello") + check "invalid string length" in exception.msg test "unexpected end of input": - check bDecode("l").l == newSeq[BencodeObj]() - check bDecode("d").d == initOrderedTable[string, BencodeObj]() - check bDecode("d5:hello5:world3:foo").d == { "hello": Bencode("world") }.toOrderedTable + const ExpectedMsg = "expected 'e'" + + var exception: ref ValueError + exception = + expect ValueError: + discard bDecode("l") + check ExpectedMsg in exception.msg + exception = + expect ValueError: + discard bDecode("d") + check ExpectedMsg in exception.msg + exception = + expect ValueError: + discard bDecode("d5:hello5:world3:foo") + check ExpectedMsg in exception.msg + exception = + expect ValueError: + echo bDecode("5") + check "expected ':'" in exception.msg test "toBencode": let world = "world" From 2845c273902e981f423052d028789cebcc75f795 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 05/38] Separate tests --- tests/{test.nim => tcore.nim} | 53 -------------------------------- tests/tjson.nim | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 53 deletions(-) rename tests/{test.nim => tcore.nim} (78%) create mode 100644 tests/tjson.nim diff --git a/tests/test.nim b/tests/tcore.nim similarity index 78% rename from tests/test.nim rename to tests/tcore.nim index a8d38e9..fa7df46 100644 --- a/tests/test.nim +++ b/tests/tcore.nim @@ -1,7 +1,6 @@ import ./utils import pkg/bencode import std/[ - json, strutils, tables, unittest, @@ -31,58 +30,6 @@ test "basic encode/decode": check bEncode(k) == v check bDecode(v) == k -test "conversion to json": - let - expected = parseJson(""" - { - "foo": 69, - "bar": [ - { - "baz": 420, - "qux": 6969, - } - ] - } - """) - actual = Bencode({ - "foo": Bencode(69), - "bar": Bencode(@[ - Bencode({ - "baz": Bencode(420), - "qux": Bencode(6969), - }), - ]), - }).toJson - - check actual == expected - -test "conversion from json": - let - expected = Bencode({ - "foo": Bencode(69), - "bar": Bencode(@[ - Bencode({ - "baz": Bencode(420), - "qux": Bencode(6969), - }), - Bencode(3), # float truncation - ]), - }) - actual = parseJson(""" - { - "foo": 69, - "bar": [ - { - "baz": 420, - "qux": 6969 - }, - 3.14159 - ] - } - """).fromJson - - check actual == expected - test "dictionary access by string key": var b = Bencode({ "interval": Bencode(1800), diff --git a/tests/tjson.nim b/tests/tjson.nim new file mode 100644 index 0000000..b943352 --- /dev/null +++ b/tests/tjson.nim @@ -0,0 +1,57 @@ +import pkg/bencode +import std/[ + json, + unittest, +] + +test "conversion to json": + let + expected = parseJson(""" + { + "foo": 69, + "bar": [ + { + "baz": 420, + "qux": 6969, + } + ] + } + """) + actual = Bencode({ + "foo": Bencode(69), + "bar": Bencode(@[ + Bencode({ + "baz": Bencode(420), + "qux": Bencode(6969), + }), + ]), + }).toJson + + check actual == expected + +test "conversion from json": + let + expected = Bencode({ + "foo": Bencode(69), + "bar": Bencode(@[ + Bencode({ + "baz": Bencode(420), + "qux": Bencode(6969), + }), + Bencode(3), # float truncation + ]), + }) + actual = parseJson(""" + { + "foo": 69, + "bar": [ + { + "baz": 420, + "qux": 6969 + }, + 3.14159 + ] + } + """).fromJson + + check actual == expected From 22531831aa91c2f286f2a676ed6619fd8528282d Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 06/38] Refactor to use jsony-style parse hooks --- bencode/core.nim | 63 +++++++++++++++++++++++++----------------------- tests/tcore.nim | 2 +- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/bencode/core.nim b/bencode/core.nim index a7292a6..bfc99a3 100644 --- a/bencode/core.nim +++ b/bencode/core.nim @@ -56,9 +56,7 @@ proc consume(s: Stream; c: char) = if actual != c: raise (ref ValueError)(msg: &"expected '{c}', got {actual}") -proc decode(s: Stream): BencodeObj - -proc decodeStr(s: Stream): BencodeObj = +proc parseHook(s: Stream; v: var string) = # : # get the length var lengthStr = "" @@ -70,66 +68,71 @@ proc decodeStr(s: Stream): BencodeObj = raise (ref ValueError)(msg: &"invalid string length: {length}") # read the string - let str = + v = if length >= 0: s.readStr(length) else: "" - if str.len != length: - raise (ref ValueError)(msg: &"string too short: expected {length} characters, got {str.len} characters") - BencodeObj(kind: bkStr, s: str) + if v.len != length: + raise (ref ValueError)(msg: &"string too short: expected {length} characters, got {v.len} characters") -proc decodeInt(s: Stream): BencodeObj = +proc parseHook(s: Stream; v: var int) = # ie consume(s, 'i') var iStr = "" while not s.atEnd and s.peekChar() != 'e': iStr &= s.readChar() consume(s, 'e') - BencodeObj(kind: bkInt, i: parseInt(iStr)) + v = parseInt(iStr) -proc decodeList(s: Stream): BencodeObj = +proc parseHook[T](s: Stream; v: var seq[T]) = # l ... e - var l = newSeq[BencodeObj]() + v = newSeq[T]() consume(s, 'l') while not s.atEnd and s.peekChar() != 'e': - l.add(decode(s)) + var item: T + parseHook(s, item) + v.add(item) consume(s, 'e') - BencodeObj(kind: bkList, l: l) -proc decodeDict(s: Stream): BencodeObj = +proc parseHook[T](s: Stream; v: var OrderedTable[string, T]) = # d ... e var - d = initOrderedTable[string, BencodeObj]() isReadingKey = true curKey = "" consume(s, 'd') while not s.atEnd and s.peekChar() != 'e': if isReadingKey: - let keyObj = decode(s) - if keyObj.kind != bkStr: - raise newException(ValueError, &"invalid dictionary key: expected {bkStr}, got {keyObj.kind}") - curKey = keyObj.s + parseHook(s, curKey) isReadingKey = false else: - d[curKey] = decode(s) + var value: T + parseHook(s, value) + v[curKey] = value isReadingKey = true consume(s, 'e') - BencodeObj(kind: bkDict, d: d) -proc decode(s: Stream): BencodeObj = +proc parseHook(s: Stream; v: var BencodeObj) = assert not s.atEnd - result = case s.peekChar() - of 'i': decodeInt(s) - of 'l': decodeList(s) - of 'd': decodeDict(s) - else: decodeStr(s) + case s.peekChar() + of 'i': + v = BencodeObj(kind: bkInt) + parseHook(s, v.i) + of 'l': + v = BencodeObj(kind: bkList) + parseHook(s, v.l) + of 'd': + v = BencodeObj(kind: bkDict) + parseHook(s, v.d) + else: + v = BencodeObj(kind: bkStr) + parseHook(s, v.s) proc bDecode*(s: Stream): BencodeObj = - decode(s) + parseHook(s, result) proc bDecode*(source: string): BencodeObj = - decode(newStringStream(source)) + bDecode(newStringStream(source)) proc bDecode*(f: File): BencodeObj = - decode(newFileStream(f)) + bDecode(newFileStream(f)) diff --git a/tests/tcore.nim b/tests/tcore.nim index fa7df46..eefec99 100644 --- a/tests/tcore.nim +++ b/tests/tcore.nim @@ -130,4 +130,4 @@ test "catch wrong dictionary key kind": let exception = expect(ValueError): discard bDecode(data) - check exception.msg == "invalid dictionary key: expected string, got integer" + check exception.msg == "invalid integer: i123e3" From c66a2cdc613e792084e4935fff09f3a59a546724 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 07/38] Add deserialization to object --- .gitignore | 2 ++ bencode/core.nim | 32 ++++++++++++++++++++++++++++++++ tests/tcore.nim | 18 ++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/.gitignore b/.gitignore index 606ee08..5e1d63a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.bin .vscode + +htmldocs diff --git a/bencode/core.nim b/bencode/core.nim index bfc99a3..b0087ff 100644 --- a/bencode/core.nim +++ b/bencode/core.nim @@ -128,6 +128,38 @@ proc parseHook(s: Stream; v: var BencodeObj) = v = BencodeObj(kind: bkStr) parseHook(s, v.s) +proc parseHook(s: Stream; v: var object) = + # d ... e + # TODO Similar to the parseHook for OrderedTable. Unify or factor them somehow? + var + isReadingKey = true + curKey = "" + consume(s, 'd') + while not s.atEnd and s.peekChar() != 'e': + if isReadingKey: + parseHook(s, curKey) + isReadingKey = false + else: + for name, value in fieldPairs(v): + if name == curKey: + parseHook(s, value) + isReadingKey = true + consume(s, 'e') + +proc fromBencode*(t: typedesc; s: Stream): t = + parseHook(s, result) + +proc fromBencode*(t: typedesc; source: string): t = + fromBencode(t, newStringStream(source)) + +proc fromBencode*(s: Stream; t: typedesc): t {.deprecated: "use fromBencode(typedesc, Stream) instead".} = + ## Logically backwards overload to match jsony's interface. + parseHook(s, result) + +proc fromBencode*(source: string; t: typedesc): t {.deprecated: "use fromBencode(typedesc, string) instead".} = + ## Logically backwards overload to match jsony's interface. + fromBencode(t, newStringStream(source)) + proc bDecode*(s: Stream): BencodeObj = parseHook(s, result) diff --git a/tests/tcore.nim b/tests/tcore.nim index eefec99..50c43fc 100644 --- a/tests/tcore.nim +++ b/tests/tcore.nim @@ -131,3 +131,21 @@ test "catch wrong dictionary key kind": expect(ValueError): discard bDecode(data) check exception.msg == "invalid integer: i123e3" + +test "deserialization to object": + type + Record = object + name: string + lang: string + age: int + alist: seq[BencodeObj] + + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + let record = Record.fromBencode(data) + check record == Record( + name: "dmdm", + lang: "nim", + age: 50, + alist: @[Bencode(1), Bencode("hi")], + ) + check record == data.fromBencode(Record) From c156197b3643f5123c3f7f8aee1535fef31aac28 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 08/38] Separate encoding and decoding --- bencode.nim | 8 ++++-- bencode/{core.nim => decoding.nim} | 42 +----------------------------- bencode/encoding.nim | 42 ++++++++++++++++++++++++++++++ bencode/json.nim | 2 +- tests/{tcore.nim => tdecoding.nim} | 26 +----------------- tests/troundtrip.nim | 32 +++++++++++++++++++++++ 6 files changed, 83 insertions(+), 69 deletions(-) rename bencode/{core.nim => decoding.nim} (81%) create mode 100644 bencode/encoding.nim rename tests/{tcore.nim => tdecoding.nim} (82%) create mode 100644 tests/troundtrip.nim diff --git a/bencode.nim b/bencode.nim index 80668df..083da71 100644 --- a/bencode.nim +++ b/bencode.nim @@ -1,6 +1,10 @@ -import ./bencode/[core, json] +import ./bencode/[ + decoding, + encoding, + json, +] -export core, json +export json, decoding, encoding when isMainModule: import std/os diff --git a/bencode/core.nim b/bencode/decoding.nim similarity index 81% rename from bencode/core.nim rename to bencode/decoding.nim index b0087ff..a8b0c77 100644 --- a/bencode/core.nim +++ b/bencode/decoding.nim @@ -1,53 +1,13 @@ +import ./types import std/[ streams, strformat, strutils, tables, ] -import ./types export types -# encode # - -proc bEncode*(obj: BencodeObj): string - -proc encodeStr(s: string): string = - $s.len & ':' & s - -proc encodeInt(i: int): string = - 'i' & $i & 'e' - -proc encodeList(l: seq[BencodeObj]): string = - result = "l" - for el in l: - result &= bEncode(el) - result &= "e" - -proc encodeDict(d: OrderedTable[string, BencodeObj]): string = - var d = d - d.sort do (x, y: tuple[key: string; value: BencodeObj]) -> int: - system.cmp(x.key, y.key) - - result = "d" - for k, v in d.pairs(): - result &= encodeStr(k) & bEncode(v) - - result &= "e" - -proc bEncode*(obj: BencodeObj): string = - result = case obj.kind - of bkStr: - encodeStr(obj.s) - of bkInt: - encodeInt(obj.i) - of bkList: - encodeList(obj.l) - of bkDict: - encodeDict(obj.d) - -# decode # - proc consume(s: Stream; c: char) = ## Check that the char at the current position is `c`, then consume it. if s.atEnd: diff --git a/bencode/encoding.nim b/bencode/encoding.nim new file mode 100644 index 0000000..ee9e037 --- /dev/null +++ b/bencode/encoding.nim @@ -0,0 +1,42 @@ +import ./types +import std/[ + tables, +] + +export types + +proc bEncode*(obj: BencodeObj): string + +proc encodeStr(s: string): string = + $s.len & ':' & s + +proc encodeInt(i: int): string = + 'i' & $i & 'e' + +proc encodeList(l: seq[BencodeObj]): string = + result = "l" + for el in l: + result &= bEncode(el) + result &= "e" + +proc encodeDict(d: OrderedTable[string, BencodeObj]): string = + var d = d + d.sort do (x, y: tuple[key: string; value: BencodeObj]) -> int: + system.cmp(x.key, y.key) + + result = "d" + for k, v in d.pairs(): + result &= encodeStr(k) & bEncode(v) + + result &= "e" + +proc bEncode*(obj: BencodeObj): string = + result = case obj.kind + of bkStr: + encodeStr(obj.s) + of bkInt: + encodeInt(obj.i) + of bkList: + encodeList(obj.l) + of bkDict: + encodeDict(obj.d) diff --git a/bencode/json.nim b/bencode/json.nim index f92bebd..3eafb74 100644 --- a/bencode/json.nim +++ b/bencode/json.nim @@ -4,7 +4,7 @@ import std/[ sugar, tables, ] -import ./core +import ./types # to # diff --git a/tests/tcore.nim b/tests/tdecoding.nim similarity index 82% rename from tests/tcore.nim rename to tests/tdecoding.nim index 50c43fc..bc6e13e 100644 --- a/tests/tcore.nim +++ b/tests/tdecoding.nim @@ -1,35 +1,11 @@ import ./utils -import pkg/bencode +import pkg/bencode/decoding import std/[ strutils, tables, unittest, ] -test "basic encode/decode": - let - myList = @[Bencode(1), Bencode("hi")] - myDict = - { - "name": Bencode("dmdm"), - "lang": Bencode("nim"), - "age": Bencode(50), - "alist": Bencode(myList), - } - testPairs = - { - Bencode("hello"): "5:hello", - Bencode("yes"): "3:yes", - Bencode(55): "i55e", - Bencode(12345): "i12345e", - Bencode(myList): "li1e2:hie", - Bencode(myDict): "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme", - }.toOrderedTable - - for k, v in testPairs.pairs: - check bEncode(k) == v - check bDecode(v) == k - test "dictionary access by string key": var b = Bencode({ "interval": Bencode(1800), diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim new file mode 100644 index 0000000..25e6051 --- /dev/null +++ b/tests/troundtrip.nim @@ -0,0 +1,32 @@ +import pkg/bencode/[ + decoding, + encoding, +] +import std/[ + tables, + unittest, +] + +test "basic encode/decode": + let + myList = @[Bencode(1), Bencode("hi")] + myDict = + { + "name": Bencode("dmdm"), + "lang": Bencode("nim"), + "age": Bencode(50), + "alist": Bencode(myList), + } + testPairs = + { + Bencode("hello"): "5:hello", + Bencode("yes"): "3:yes", + Bencode(55): "i55e", + Bencode(12345): "i12345e", + Bencode(myList): "li1e2:hie", + Bencode(myDict): "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme", + }.toOrderedTable + + for k, v in testPairs.pairs: + check bEncode(k) == v + check bDecode(v) == k From 884406b8ebd482be2a98556053cebbfb3c5d3769 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 09/38] Support decoding to Table Also export the parseHook procs. --- bencode/decoding.nim | 21 +++++++++++++++------ tests/tdecoding.nim | 11 +++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index a8b0c77..2e5e563 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -16,7 +16,7 @@ proc consume(s: Stream; c: char) = if actual != c: raise (ref ValueError)(msg: &"expected '{c}', got {actual}") -proc parseHook(s: Stream; v: var string) = +proc parseHook*(s: Stream; v: var string) = # : # get the length var lengthStr = "" @@ -36,7 +36,7 @@ proc parseHook(s: Stream; v: var string) = if v.len != length: raise (ref ValueError)(msg: &"string too short: expected {length} characters, got {v.len} characters") -proc parseHook(s: Stream; v: var int) = +proc parseHook*(s: Stream; v: var int) = # ie consume(s, 'i') var iStr = "" @@ -45,7 +45,7 @@ proc parseHook(s: Stream; v: var int) = consume(s, 'e') v = parseInt(iStr) -proc parseHook[T](s: Stream; v: var seq[T]) = +proc parseHook*[T](s: Stream; v: var seq[T]) = # l ... e v = newSeq[T]() consume(s, 'l') @@ -55,7 +55,9 @@ proc parseHook[T](s: Stream; v: var seq[T]) = v.add(item) consume(s, 'e') -proc parseHook[T](s: Stream; v: var OrderedTable[string, T]) = +type SomeTable[K, V] = Table[K, V] or OrderedTable[K, V] + +proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = # d ... e var isReadingKey = true @@ -72,7 +74,14 @@ proc parseHook[T](s: Stream; v: var OrderedTable[string, T]) = isReadingKey = true consume(s, 'e') -proc parseHook(s: Stream; v: var BencodeObj) = +proc parseHook*[T](s: Stream; v: var OrderedTable[string, T]) = + # TODO why is this needed? + parseHookTableImpl(s, v) + +proc parseHook*[T](s: Stream; v: var Table[string, T]) = + parseHookTableImpl(s, v) + +proc parseHook*(s: Stream; v: var BencodeObj) = assert not s.atEnd case s.peekChar() of 'i': @@ -88,7 +97,7 @@ proc parseHook(s: Stream; v: var BencodeObj) = v = BencodeObj(kind: bkStr) parseHook(s, v.s) -proc parseHook(s: Stream; v: var object) = +proc parseHook*(s: Stream; v: var object) = # d ... e # TODO Similar to the parseHook for OrderedTable. Unify or factor them somehow? var diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index bc6e13e..a58e5ab 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -125,3 +125,14 @@ test "deserialization to object": alist: @[Bencode(1), Bencode("hi")], ) check record == data.fromBencode(Record) + +test "various table types": + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + let expectedTablePairs = { + "age": Bencode(50), + "alist": Bencode(@[Bencode(1), Bencode("hi")]), + "lang": Bencode("nim"), + "name": Bencode("dmdm"), + } + check OrderedTable[string, BencodeObj].fromBencode(data) == expectedTablePairs.toOrderedTable + check Table[string, BencodeObj].fromBencode(data) == expectedTablePairs.toTable From d5401be02b6781300e381f7331d87d58743a05c9 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 10/38] Support decoding to ref object --- bencode/decoding.nim | 4 ++++ tests/tdecoding.nim | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 2e5e563..1a95fdb 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -115,6 +115,10 @@ proc parseHook*(s: Stream; v: var object) = isReadingKey = true consume(s, 'e') +proc parseHook*[T: ref object](s: Stream; v: var T) = + v = T() + parseHook(s, v[]) + proc fromBencode*(t: typedesc; s: Stream): t = parseHook(s, result) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index a58e5ab..41212bf 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -108,7 +108,7 @@ test "catch wrong dictionary key kind": discard bDecode(data) check exception.msg == "invalid integer: i123e3" -test "deserialization to object": +test "deserialization to (ref) object": type Record = object name: string @@ -116,15 +116,20 @@ test "deserialization to object": age: int alist: seq[BencodeObj] - const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" - let record = Record.fromBencode(data) - check record == Record( + let expectedRecord = Record( name: "dmdm", lang: "nim", age: 50, alist: @[Bencode(1), Bencode("hi")], ) - check record == data.fromBencode(Record) + + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + check Record.fromBencode(data) == expectedRecord + let refRecord = (ref Record).fromBencode(data) + check refRecord != nil + check refRecord[] == expectedRecord + # test the nonsensical overload + check data.fromBencode(Record) == expectedRecord test "various table types": const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" From f1154ce8fb6db42890fcb0b39971affcc2c0c74f Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 11/38] Support decoding to JsonNode --- bencode/decoding.nim | 26 +++++++++++++++++++++++++- tests/tdecoding.nim | 12 ++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 1a95fdb..6d221b7 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -97,7 +97,31 @@ proc parseHook*(s: Stream; v: var BencodeObj) = v = BencodeObj(kind: bkStr) parseHook(s, v.s) -proc parseHook*(s: Stream; v: var object) = +import std/json + +proc parseHook*(s: Stream; v: var JsonNode) = + assert not s.atEnd + case s.peekChar() + of 'i': + var value: int + parseHook(s, value) + v = newJInt(value) + of 'l': + var value: seq[JsonNode] + parseHook(s, value) + v = newJArray() + v.elems = value + of 'd': + var value: OrderedTable[string, JsonNode] + parseHook(s, value) + v = newJObject() + v.fields = value + else: + var value: string + parseHook(s, value) + v = newJString(value) + +proc parseHook*[T: object](s: Stream; v: var T) = # d ... e # TODO Similar to the parseHook for OrderedTable. Unify or factor them somehow? var diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 41212bf..043604c 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -141,3 +141,15 @@ test "various table types": } check OrderedTable[string, BencodeObj].fromBencode(data) == expectedTablePairs.toOrderedTable check Table[string, BencodeObj].fromBencode(data) == expectedTablePairs.toTable + +import std/json + +test "deserialization to JsonNode": + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + let expected = %*{ + "age": 50, + "alist": [1, "hi"], + "lang": "nim", + "name": "dmdm", + } + check JsonNode.fromBencode(data) == expected From e1dbd376afb910c7f6f7fdf2ee7774d00bcbedac Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 12/38] Require explicit import to use json module --- bencode.nim | 3 +-- tests/tjson.nim | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bencode.nim b/bencode.nim index 083da71..dee4ce1 100644 --- a/bencode.nim +++ b/bencode.nim @@ -1,10 +1,9 @@ import ./bencode/[ decoding, encoding, - json, ] -export json, decoding, encoding +export decoding, encoding when isMainModule: import std/os diff --git a/tests/tjson.nim b/tests/tjson.nim index b943352..ead3a33 100644 --- a/tests/tjson.nim +++ b/tests/tjson.nim @@ -1,4 +1,5 @@ import pkg/bencode +import pkg/bencode/json import std/[ json, unittest, From 131e641b401deb9d4d184e911bb7d5e090d9ddf5 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 13/38] Support nimPreviewSlimSystem --- bencode.nim | 5 ++++- bencode/decoding.nim | 2 ++ tests/treadme.nim | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bencode.nim b/bencode.nim index dee4ce1..45ec838 100644 --- a/bencode.nim +++ b/bencode.nim @@ -6,7 +6,10 @@ import ./bencode/[ export decoding, encoding when isMainModule: - import std/os + import std/[ + os, + syncio, + ] proc parseFormatArg(arg: string): BencodeFormat = if arg.len < 2: quit("Invalid argument.") diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 6d221b7..588b012 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,8 +1,10 @@ import ./types import std/[ + assertions, streams, strformat, strutils, + syncio, tables, ] diff --git a/tests/treadme.nim b/tests/treadme.nim index f455694..f9592e4 100644 --- a/tests/treadme.nim +++ b/tests/treadme.nim @@ -1,4 +1,5 @@ import pkg/bencode +import std/assertions let data = be({ From 1b9cc38c5667d8f37446cc6477212125fd6b5702 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 14/38] Support strictDefs --- bencode/decoding.nim | 15 +++++++++------ bencode/types.nim | 2 +- tests/tdecoding.nim | 8 +++++++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 588b012..c6ab18c 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -52,7 +52,7 @@ proc parseHook*[T](s: Stream; v: var seq[T]) = v = newSeq[T]() consume(s, 'l') while not s.atEnd and s.peekChar() != 'e': - var item: T + var item = default T parseHook(s, item) v.add(item) consume(s, 'e') @@ -70,10 +70,11 @@ proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = parseHook(s, curKey) isReadingKey = false else: - var value: T + var value = default T parseHook(s, value) v[curKey] = value isReadingKey = true + # TODO raise on incomplete pair consume(s, 'e') proc parseHook*[T](s: Stream; v: var OrderedTable[string, T]) = @@ -105,21 +106,21 @@ proc parseHook*(s: Stream; v: var JsonNode) = assert not s.atEnd case s.peekChar() of 'i': - var value: int + var value = default int parseHook(s, value) v = newJInt(value) of 'l': - var value: seq[JsonNode] + var value = default seq[JsonNode] parseHook(s, value) v = newJArray() v.elems = value of 'd': - var value: OrderedTable[string, JsonNode] + var value = default OrderedTable[string, JsonNode] parseHook(s, value) v = newJObject() v.fields = value else: - var value: string + var value = default string parseHook(s, value) v = newJString(value) @@ -146,6 +147,7 @@ proc parseHook*[T: ref object](s: Stream; v: var T) = parseHook(s, v[]) proc fromBencode*(t: typedesc; s: Stream): t = + result = default t parseHook(s, result) proc fromBencode*(t: typedesc; source: string): t = @@ -160,6 +162,7 @@ proc fromBencode*(source: string; t: typedesc): t {.deprecated: "use fromBencode fromBencode(t, newStringStream(source)) proc bDecode*(s: Stream): BencodeObj = + result = BencodeObj() parseHook(s, result) proc bDecode*(source: string): BencodeObj = diff --git a/bencode/types.nim b/bencode/types.nim index d7f78f8..f31d1c0 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -62,7 +62,7 @@ func hash*(obj: BencodeObj): Hash = of bkInt: !$(hash(obj.i)) of bkList: !$(hash(obj.l)) of bkDict: - var h: Hash + var h = default Hash for k, v in obj.d.pairs: h = hash(k) !& hash(v) !$(h) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 043604c..e35be58 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -115,21 +115,27 @@ test "deserialization to (ref) object": lang: string age: int alist: seq[BencodeObj] + blist: seq[int] + mydict: OrderedTable[string, string] let expectedRecord = Record( name: "dmdm", lang: "nim", age: 50, alist: @[Bencode(1), Bencode("hi")], + blist: @[100, 200], + mydict: {"foo": "bar"}.toOrderedTable, ) - const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" check Record.fromBencode(data) == expectedRecord let refRecord = (ref Record).fromBencode(data) check refRecord != nil check refRecord[] == expectedRecord # test the nonsensical overload + {.push warning[Deprecated]:off.} check data.fromBencode(Record) == expectedRecord + {.pop.} test "various table types": const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" From dbeabe2e0ac4d607d226807d9fc6092602469ebb Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 15/38] Docs --- bencode/decoding.nim | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index c6ab18c..5da4e0f 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -147,10 +147,25 @@ proc parseHook*[T: ref object](s: Stream; v: var T) = parseHook(s, v[]) proc fromBencode*(t: typedesc; s: Stream): t = + ## Decode bencoded data from `s` into a value of type `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: + type Foo = object + a: int + b: string + c: BencodeObj + + let data = "d1:b11:hello world1:ai42e1:c16:embedded bencodee" + doAssert Foo.fromBencode(data) == Foo( + a: 42, + b: "hello world", + c: Bencode("embedded bencode"), + ) + fromBencode(t, newStringStream(source)) proc fromBencode*(s: Stream; t: typedesc): t {.deprecated: "use fromBencode(typedesc, Stream) instead".} = @@ -162,11 +177,12 @@ proc fromBencode*(source: string; t: typedesc): t {.deprecated: "use fromBencode fromBencode(t, newStringStream(source)) proc bDecode*(s: Stream): BencodeObj = - result = BencodeObj() - parseHook(s, result) + ## Same as `BencodeObj.fromBencode(s)`. + fromBencode(BencodeObj, s) proc bDecode*(source: string): BencodeObj = - bDecode(newStringStream(source)) + ## Same as `BencodeObj.fromBencode(source)`. + fromBencode(BencodeObj, source) proc bDecode*(f: File): BencodeObj = - bDecode(newFileStream(f)) + fromBencode(BencodeObj, newFileStream(f)) From c6c5bfb164101b656da260db41d7e7d41190622f Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 16/38] Rename toBencode to toBencodeObj `toBencode` will be used for the object-to-bencode-string proc once it exists. --- bencode/types.nim | 61 +++++++++++++++++++++++++-------------------- tests/tdecoding.nim | 36 -------------------------- tests/ttypes.nim | 40 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 tests/ttypes.nim diff --git a/bencode/types.nim b/bencode/types.nim index f31d1c0..32fad71 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -90,25 +90,45 @@ func `==`*(a, b: BencodeObj): bool = # constructors # -proc Bencode*(s: sink string): BencodeObj = +proc `name=`(procDef, name: NimNode) = + procDef.expectKind nnkProcDef + name.expectKind {nnkIdent, nnkSym} + + case procDef[0].kind + of nnkPostfix: + procDef[0][1] = name + of nnkIdent: + procDef[0] = name + else: + error("unexpected node kind", procDef[0]) + +macro alias(name, procDef: untyped): untyped = + procDef.expectKind nnkProcDef + name.expectKind {nnkIdent, nnkSym} + + let aliasProcDef = procDef.copy + aliasProcDef.name = name + newStmtList(procDef, aliasProcDef) + +proc Bencode*(s: sink string): BencodeObj {.alias: be.} = BencodeObj(kind: bkStr, s: s) -proc Bencode*(i: int): BencodeObj = +proc Bencode*(i: int): BencodeObj {.alias: be.} = BencodeObj(kind: bkInt, i: i) -proc Bencode*(l: sink seq[BencodeObj]): BencodeObj = +proc Bencode*(l: sink seq[BencodeObj]): BencodeObj {.alias: be.} = BencodeObj(kind: bkList, l: l) -proc Bencode*(l: sink openArray[BencodeObj]): BencodeObj = +proc Bencode*(l: sink openArray[BencodeObj]): BencodeObj {.alias: be.} = BencodeObj(kind: bkList, l: l.toSeq) -proc Bencode*(d: sink OrderedTable[string, BencodeObj]): BencodeObj = +proc Bencode*(d: sink OrderedTable[string, BencodeObj]): BencodeObj {.alias: be.} = BencodeObj(kind: bkDict, d: d) -proc Bencode*(d: sink openArray[(string, BencodeObj)]): BencodeObj = +proc Bencode*(d: sink openArray[(string, BencodeObj)]): BencodeObj {.alias: be.} = Bencode(d.toOrderedTable) -func toBencodeImpl(value: NimNode): NimNode = +func toBencodeObjImpl(value: NimNode): NimNode = # Adapted from std/json's `%*`: https://github.com/nim-lang/Nim/blob/0b44840299c15faa3b74cb82f48dcd56023f7d35/lib/pure/json.nim#L411 case value.kind of nnkBracket: # array @@ -117,7 +137,7 @@ func toBencodeImpl(value: NimNode): NimNode = else: var bracketNode = nnkBracket.newNimNode() for i in 0 ..< value.len: - bracketNode.add(toBencodeImpl(value[i])) + bracketNode.add(toBencodeObjImpl(value[i])) newCall(bindSym("Bencode", brOpen), bracketNode) of nnkTableConstr: # object if value.len == 0: @@ -126,32 +146,19 @@ func toBencodeImpl(value: NimNode): NimNode = var tableNode = nnkTableConstr.newNimNode() for i in 0 ..< value.len: value[i].expectKind nnkExprColonExpr - tableNode.add nnkExprColonExpr.newTree(value[i][0], toBencodeImpl(value[i][1])) + tableNode.add nnkExprColonExpr.newTree(value[i][0], toBencodeObjImpl(value[i][1])) newCall(bindSym("Bencode", brOpen), tableNode) of nnkPar: if value.len == 1: - toBencodeImpl(value[0]) + toBencodeObjImpl(value[0]) else: # what is this? newCall(bindSym("Bencode", brOpen), value) else: newCall(bindSym("Bencode", brOpen), value) -macro toBencode*(value: untyped): untyped = - toBencodeImpl(value) - -template be*(strVal: string): BencodeObj = - BencodeObj(kind: bkStr, s: strVal) - -template be*(intVal: int): BencodeObj = - BencodeObj(kind: bkInt, i: intVal) - -template be*(listVal: seq[BencodeObj]): BencodeObj = - BencodeObj(kind: bkList, l: listVal) - -template be*(dictVal: OrderedTable[string, BencodeObj]): BencodeObj = - BencodeObj(kind: bkDict, d: dictVal) +macro toBencodeObj*(value: untyped): BencodeObj = + toBencodeObjImpl(value) -template be*(dictVal: openArray[(string, BencodeObj)]): BencodeObj = - mixin toOrderedTable - BencodeObj(kind: bkDict, d: dictVal.toOrderedTable) +macro toBencode*(value: untyped): untyped {.deprecated: "use toBencodeObj instead".} = + toBencodeObjImpl(value) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index e35be58..a160870 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -65,42 +65,6 @@ test "unexpected end of input": echo bDecode("5") check "expected ':'" in exception.msg -test "toBencode": - let world = "world" - - func getValue(): int = - 314159 - - let actual = toBencode({ - "foo": [1, 2, 3], - "bar": { - "nested": getValue(), - "nested2": [ - { - "bar": "hello " & world, - }, - ], - }, - "paren": (3 + 4), - "empty list": [], - "empty dict": {:}, - }) - let expected = Bencode({ - "foo": Bencode([Bencode(1), Bencode(2), Bencode(3)]), - "bar": Bencode({ - "nested": Bencode(314159), - "nested2": Bencode([ - Bencode({ - "bar": Bencode("hello world"), - }) - ]), - }), - "paren": Bencode(7), - "empty list": BencodeObj(kind: bkList), - "empty dict": BencodeObj(kind: bkDict), - }) - check actual == expected - test "catch wrong dictionary key kind": const data = "d4:name4:dmdmi123e3:nim3:agei50e5:alistli1e2:hiee" let exception = diff --git a/tests/ttypes.nim b/tests/ttypes.nim new file mode 100644 index 0000000..0febb27 --- /dev/null +++ b/tests/ttypes.nim @@ -0,0 +1,40 @@ +import pkg/bencode/types +import std/[ + unittest, +] + +test "toBencodeObj": + let world = "world" + + func getValue(): int = + 314159 + + let actual = toBencodeObj({ + "foo": [1, 2, 3], + "bar": { + "nested": getValue(), + "nested2": [ + { + "bar": "hello " & world, + }, + ], + }, + "paren": (3 + 4), + "empty list": [], + "empty dict": {:}, + }) + let expected = Bencode({ + "foo": Bencode([Bencode(1), Bencode(2), Bencode(3)]), + "bar": Bencode({ + "nested": Bencode(314159), + "nested2": Bencode([ + Bencode({ + "bar": Bencode("hello world"), + }) + ]), + }), + "paren": Bencode(7), + "empty list": BencodeObj(kind: bkList), + "empty dict": BencodeObj(kind: bkDict), + }) + check actual == expected From 580a6fe4bb25142a076e34e23179227a29b6d0bf Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 17/38] Add equality and stringify tests --- tests/tdecoding.nim | 14 -------------- tests/ttypes.nim | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index a160870..4806b4d 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -6,20 +6,6 @@ import std/[ unittest, ] -test "dictionary access by string key": - var b = Bencode({ - "interval": Bencode(1800), - "complete": Bencode(20), - }) - check b.d["interval"] == Bencode(1800) - b.d["complete"] = Bencode(30) - check b.d["complete"] == Bencode(30) - - check b == be({ - "interval": be(1800), - "complete": be(30), - }) - test "execution terminates for invalid bencode input": const data = "d4:name4:dmdm4:lang3:nim3:agei50e5:alistli1e2:hiee" for i in 0 .. data.high: diff --git a/tests/ttypes.nim b/tests/ttypes.nim index 0febb27..61986a8 100644 --- a/tests/ttypes.nim +++ b/tests/ttypes.nim @@ -1,8 +1,23 @@ import pkg/bencode/types import std/[ + tables, unittest, ] +test "dictionary access by string key": + var b = Bencode({ + "interval": Bencode(1800), + "complete": Bencode(20), + }) + check b.d["interval"] == Bencode(1800) + b.d["complete"] = Bencode(30) + check b.d["complete"] == Bencode(30) + + check b == be({ + "interval": be(1800), + "complete": be(30), + }) + test "toBencodeObj": let world = "world" @@ -38,3 +53,29 @@ test "toBencodeObj": "empty dict": BencodeObj(kind: bkDict), }) check actual == expected + +template checkEquals(a, b: BencodeObj) = + check a == b + check hash(a) == hash(b) + +template checkNotEquals(a, b: BencodeObj) = + check a != b + check hash(a) != hash(b) + +test "equality": + checkEquals be"hello", be"hello" + checkNotEquals be"hello", be"world" + checkEquals be(100), be(100) + checkNotEquals be(100), be(200) + checkEquals be([be"hello", be"world"]), Bencode([be"hello", be"world"]) + checkNotEquals be([be"hello", be"world"]), Bencode([be"world", be"hello"]) + checkEquals be({"hello": be"world", "world": be([be"foo", be(1)])}), be({"hello": be"world", "world": be([be"foo", be(1)])}) + checkNotEquals be({"hello": be"world", "world": be([be"foo", be(1)])}), be({"hello": be"world", "world": be([be"foo", be(2)])}) + +test "stringify": + check $be("hello") == """"hello"""" + check be("hello").toString(Normal) == """"hello"""" + check be("hello").toString(Hexadecimal) == """"\x68\x65\x6C\x6C\x6F"""" + check be("hello").toString(Decimal) == """"\d0104\d0101\d0108\d0108\d0111"""" + check $be([be"hello", be(2)]) == """@["hello", 2]""" + check $be({"hello": be"world", "world": be([be"foo", be(1)])}) == """{ hello: "world", world: @["foo", 1] }""" From 940de6c9415b14eea250d07a30d92fdcb7c21a79 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 18/38] Various changes --- bencode/decoding.nim | 8 ++-- bencode/encoding.nim | 8 ++-- bencode/json.nim | 8 ++-- bencode/types.nim | 104 ++++++++++++++++++++++++++----------------- tests/ttypes.nim | 4 +- 5 files changed, 78 insertions(+), 54 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 5da4e0f..02126cc 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -88,16 +88,16 @@ proc parseHook*(s: Stream; v: var BencodeObj) = assert not s.atEnd case s.peekChar() of 'i': - v = BencodeObj(kind: bkInt) + v = BencodeObj(kind: Int) parseHook(s, v.i) of 'l': - v = BencodeObj(kind: bkList) + v = BencodeObj(kind: List) parseHook(s, v.l) of 'd': - v = BencodeObj(kind: bkDict) + v = BencodeObj(kind: Dict) parseHook(s, v.d) else: - v = BencodeObj(kind: bkStr) + v = BencodeObj(kind: Str) parseHook(s, v.s) import std/json diff --git a/bencode/encoding.nim b/bencode/encoding.nim index ee9e037..e41742b 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -32,11 +32,11 @@ proc encodeDict(d: OrderedTable[string, BencodeObj]): string = proc bEncode*(obj: BencodeObj): string = result = case obj.kind - of bkStr: + of Str: encodeStr(obj.s) - of bkInt: + of Int: encodeInt(obj.i) - of bkList: + of List: encodeList(obj.l) - of bkDict: + of Dict: encodeDict(obj.d) diff --git a/bencode/json.nim b/bencode/json.nim index 3eafb74..0ea87a3 100644 --- a/bencode/json.nim +++ b/bencode/json.nim @@ -18,15 +18,15 @@ proc newJObject(fields: OrderedTable[string, JsonNode]): JsonNode = proc toJson*(obj: BencodeObj): JsonNode = case obj.kind - of bkStr: + of Str: newJString(obj.s) - of bkInt: + of Int: newJInt(obj.i) - of bkList: + of List: newJArray( collect(newSeq, for x in obj.l: x.toJson) ) - of bkDict: + of Dict: newJObject( collect(initOrderedTable, for k, v in obj.d.pairs: {k: v.toJson} diff --git a/bencode/types.nim b/bencode/types.nim index 32fad71..e2ca452 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -1,67 +1,91 @@ import std/[ + enumerate, hashes, macros, sequtils, strutils, - sugar, tables, ] type BencodeKind* = enum - bkStr = "string" - bkInt = "integer" - bkList = "list" - bkDict = "dictionary" + Str = "string" + Int = "integer" + List = "list" + Dict = "dictionary" BencodeObj* = object case kind*: BencodeKind - of bkStr: + of Str: s*: string - of bkInt: + of Int: i*: int - of bkList: + of List: l*: seq[BencodeObj] - of bkDict: + of Dict: d*: OrderedTable[string, BencodeObj] BencodeFormat* = enum Normal Hexadecimal Decimal +const + bkStr* {.deprecated: "use Str instead".} = BencodeKind.Str + bkInt* {.deprecated: "use Int instead".} = BencodeKind.Int + bkList* {.deprecated: "use List instead".} = BencodeKind.List + bkDict* {.deprecated: "use Dict instead".} = BencodeKind.Dict + # $ # -func toString*(a: BencodeObj; f = Normal): string +func toString*(a: BencodeObj; f = Normal): string {.raises: [].} func toString(str: string; f = Normal): string = + result = "" case f - of Hexadecimal: str.map(c => "\\x" & ord(c).toHex(2)).join("") - of Decimal: str.map(c => "\\d" & ord(c).`$`.align(4, '0')).join("") - else: str + of Hexadecimal: + for c in str: + result.add "\\x" & c.ord.toHex(2) + of Decimal: + for c in str: + result.add "\\d" & ($c.ord).align(4, '0') + else: + result = str func toString(l: seq[BencodeObj]; f = Normal): string = - "@[" & l.map(obj => obj.toString(f)).join(", ") & "]" + result = "@[" + for i, obj in l.pairs: + if i != 0: + result &= ", " + result &= obj.toString(f) + result &= "]" func toString(d: OrderedTable[string, BencodeObj]; f = Normal): string = - "{ " & collect(newSeq, for k, v in d.pairs: k.toString(f) & ": " & v.toString(f)).join(", ") & " }" + result = "{ " + for i, (k, v) in enumerate(d.pairs): + if i != 0: + result &= ", " + result &= k.toString(f) + result &= ": " + result &= v.toString(f) + result &= " }" func toString*(a: BencodeObj; f = Normal): string = case a.kind - of bkStr: '"' & a.s.toString(f) & '"' - of bkInt: $a.i - of bkList: a.l.toString(f) - of bkDict: a.d.toString(f) + of Str: '"' & a.s.toString(f) & '"' + of Int: $a.i + of List: a.l.toString(f) + of Dict: a.d.toString(f) -func `$`*(a: BencodeObj): string = +func `$`*(a: BencodeObj): string {.raises: [].} = a.toString(Normal) # equality # func hash*(obj: BencodeObj): Hash = case obj.kind - of bkStr: !$(hash(obj.s)) - of bkInt: !$(hash(obj.i)) - of bkList: !$(hash(obj.l)) - of bkDict: + of Str: !$(hash(obj.s)) + of Int: !$(hash(obj.i)) + of List: !$(hash(obj.l)) + of Dict: var h = default Hash for k, v in obj.d.pairs: h = hash(k) !& hash(v) @@ -69,16 +93,16 @@ func hash*(obj: BencodeObj): Hash = func `==`*(a, b: BencodeObj): bool = if a.kind != b.kind: - result = false + false else: case a.kind - of bkStr: - result = a.s == b.s - of bkInt: - result = a.i == b.i - of bkList: - result = a.l == b.l - of bkDict: + of Str: + a.s == b.s + of Int: + a.i == b.i + of List: + a.l == b.l + of Dict: if a.d.len != b.d.len: return false for key in a.d.keys: @@ -86,7 +110,7 @@ func `==`*(a, b: BencodeObj): bool = return false if a.d[key] != b.d[key]: return false - result = true + true # constructors # @@ -111,19 +135,19 @@ macro alias(name, procDef: untyped): untyped = newStmtList(procDef, aliasProcDef) proc Bencode*(s: sink string): BencodeObj {.alias: be.} = - BencodeObj(kind: bkStr, s: s) + BencodeObj(kind: Str, s: s) proc Bencode*(i: int): BencodeObj {.alias: be.} = - BencodeObj(kind: bkInt, i: i) + BencodeObj(kind: Int, i: i) proc Bencode*(l: sink seq[BencodeObj]): BencodeObj {.alias: be.} = - BencodeObj(kind: bkList, l: l) + BencodeObj(kind: List, l: l) proc Bencode*(l: sink openArray[BencodeObj]): BencodeObj {.alias: be.} = - BencodeObj(kind: bkList, l: l.toSeq) + BencodeObj(kind: List, l: l.toSeq) proc Bencode*(d: sink OrderedTable[string, BencodeObj]): BencodeObj {.alias: be.} = - BencodeObj(kind: bkDict, d: d) + BencodeObj(kind: Dict, d: d) proc Bencode*(d: sink openArray[(string, BencodeObj)]): BencodeObj {.alias: be.} = Bencode(d.toOrderedTable) @@ -133,7 +157,7 @@ func toBencodeObjImpl(value: NimNode): NimNode = case value.kind of nnkBracket: # array if value.len == 0: - quote: BencodeObj(kind: bkList) + quote: BencodeObj(kind: List) else: var bracketNode = nnkBracket.newNimNode() for i in 0 ..< value.len: @@ -141,7 +165,7 @@ func toBencodeObjImpl(value: NimNode): NimNode = newCall(bindSym("Bencode", brOpen), bracketNode) of nnkTableConstr: # object if value.len == 0: - quote: BencodeObj(kind: bkDict) + quote: BencodeObj(kind: Dict) else: var tableNode = nnkTableConstr.newNimNode() for i in 0 ..< value.len: diff --git a/tests/ttypes.nim b/tests/ttypes.nim index 61986a8..edb3e5c 100644 --- a/tests/ttypes.nim +++ b/tests/ttypes.nim @@ -49,8 +49,8 @@ test "toBencodeObj": ]), }), "paren": Bencode(7), - "empty list": BencodeObj(kind: bkList), - "empty dict": BencodeObj(kind: bkDict), + "empty list": BencodeObj(kind: List), + "empty dict": BencodeObj(kind: Dict), }) check actual == expected From 06733421f83e2e2e902e4f552ad7aba47fee88dd Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 19/38] Require Nim 2 --- .github/workflows/test.yaml | 2 +- bencode.nimble | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 46a3769..30caf3d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ jobs: matrix: nimVersion: - stable - - 1.6.20 + - 2.0.8 nimMm: - orc - arc diff --git a/bencode.nimble b/bencode.nimble index 16ae322..ffc4347 100644 --- a/bencode.nimble +++ b/bencode.nimble @@ -9,4 +9,4 @@ installExt = @["nim"] # Dependencies -requires "nim >= 1.6.20" +requires "nim >= 2.0.8" From 4b3a99dbfcbdca8eb1b9e5b610b14be17aff6901 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 20/38] Decode to array --- bencode/decoding.nim | 14 ++++++++++++++ tests/tdecoding.nim | 10 +++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 02126cc..4bd2f8d 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -57,6 +57,20 @@ proc parseHook*[T](s: Stream; v: var seq[T]) = v.add(item) consume(s, 'e') +proc parseHook*[T; C: static int](s: Stream; 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 (ref ValueError)(msg: &"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') + type SomeTable[K, V] = Table[K, V] or OrderedTable[K, V] proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 4806b4d..0d1fc5a 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -67,6 +67,7 @@ test "deserialization to (ref) object": alist: seq[BencodeObj] blist: seq[int] mydict: OrderedTable[string, string] + myarray: array[2, string] let expectedRecord = Record( name: "dmdm", @@ -75,9 +76,10 @@ test "deserialization to (ref) object": alist: @[Bencode(1), Bencode("hi")], blist: @[100, 200], mydict: {"foo": "bar"}.toOrderedTable, + myarray: ["hello", "world"] ) - const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" + const data = "d3:agei50e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" check Record.fromBencode(data) == expectedRecord let refRecord = (ref Record).fromBencode(data) check refRecord != nil @@ -109,3 +111,9 @@ test "deserialization to JsonNode": "name": "dmdm", } check JsonNode.fromBencode(data) == expected + +test "list too long for array": + let exception = + expect ValueError: + discard array[2, string].fromBencode("l5:hello5:world2:!!ee") + check exception.msg == "list too long: expected 2 items, got at least 3 items" From b66e4c9dd5e904418ad4169830b73cb12f49380c Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 21/38] BencodeDecodeError type --- bencode/decoding.nim | 37 +++++++++++++++++++++++++++++-------- tests/tdecoding.nim | 31 +++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 4bd2f8d..19dd309 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,22 +1,43 @@ import ./types import std/[ assertions, + parseutils, streams, strformat, - strutils, syncio, tables, ] export types +type + BencodeDecodeErrorKind* = enum + SyntaxError + UnexpectedEndOfInput + WrongLength + InvalidValue + BencodeDecodeError* = object of ValueError + 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: Stream; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError = + newBencodeDecodeError(s.getPosition, kind, msg) + proc consume(s: Stream; c: char) = ## Check that the char at the current position is `c`, then consume it. if s.atEnd: - raise (ref ValueError)(msg: &"expected '{c}', got end of input") + raise newBencodeDecodeError(s, UnexpectedEndOfInput, &"expected '{c}', got end of input") let actual = s.readChar() if actual != c: - raise (ref ValueError)(msg: &"expected '{c}', got {actual}") + raise newBencodeDecodeError(s, SyntaxError, &"expected '{c}', got {actual}") + +proc parseInt(str: string; pos: int): int = + result = 0 # parseutils.parseInt's second parameter really should be marked `out` + if parseutils.parseInt(str, result) != str.len: + raise newBencodeDecodeError(pos, SyntaxError, &"invalid integer: {str}") proc parseHook*(s: Stream; v: var string) = # : @@ -25,9 +46,9 @@ proc parseHook*(s: Stream; v: var string) = while not s.atEnd and s.peekChar() != ':': lengthStr &= s.readChar() consume(s, ':') - let length = parseInt(lengthStr) + let length = parseInt(lengthStr, s.getPosition) if length < 0: - raise (ref ValueError)(msg: &"invalid string length: {length}") + raise newBencodeDecodeError(s, InvalidValue, &"invalid string length: {length}") # read the string v = @@ -36,7 +57,7 @@ proc parseHook*(s: Stream; v: var string) = else: "" if v.len != length: - raise (ref ValueError)(msg: &"string too short: expected {length} characters, got {v.len} characters") + raise newBencodeDecodeError(s, WrongLength, &"string too short: expected {length} characters, got {v.len} characters") proc parseHook*(s: Stream; v: var int) = # ie @@ -45,7 +66,7 @@ proc parseHook*(s: Stream; v: var int) = while not s.atEnd and s.peekChar() != 'e': iStr &= s.readChar() consume(s, 'e') - v = parseInt(iStr) + v = parseInt(iStr, s.getPosition) proc parseHook*[T](s: Stream; v: var seq[T]) = # l ... e @@ -64,7 +85,7 @@ proc parseHook*[T; C: static int](s: Stream; v: var array[C, T]) = var i = 0 while not s.atEnd and s.peekChar() != 'e': if i >= C: - raise (ref ValueError)(msg: &"list too long: expected {C} items, got at least {i + 1} items") + 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 diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 0d1fc5a..3cbfeba 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -15,47 +15,57 @@ test "execution terminates for invalid bencode input": let invalidData = data[0 .. i - 1] & data[i + 1 .. ^1] try: discard bDecode(invalidData) - except ValueError: + except BencodeDecodeError: discard test "string too short": let exception = - expect ValueError: + expect BencodeDecodeError: discard bDecode("10:hello") + check exception.kind == WrongLength check "string too short" in exception.msg test "invalid string length": let exception = - expect ValueError: + expect BencodeDecodeError: discard bDecode("-5:hello") + check exception.kind == InvalidValue check "invalid string length" in exception.msg test "unexpected end of input": const ExpectedMsg = "expected 'e'" - var exception: ref ValueError + var exception: ref BencodeDecodeError exception = - expect ValueError: + expect BencodeDecodeError: discard bDecode("l") + check exception.kind == UnexpectedEndOfInput check ExpectedMsg in exception.msg + exception = - expect ValueError: + expect BencodeDecodeError: discard bDecode("d") + check exception.kind == UnexpectedEndOfInput check ExpectedMsg in exception.msg + exception = - expect ValueError: + expect BencodeDecodeError: discard bDecode("d5:hello5:world3:foo") + check exception.kind == UnexpectedEndOfInput check ExpectedMsg in exception.msg + exception = - expect ValueError: + expect BencodeDecodeError: echo bDecode("5") + check exception.kind == UnexpectedEndOfInput check "expected ':'" in exception.msg test "catch wrong dictionary key kind": const data = "d4:name4:dmdmi123e3:nim3:agei50e5:alistli1e2:hiee" let exception = - expect(ValueError): + expect BencodeDecodeError: discard bDecode(data) + check exception.kind == SyntaxError check exception.msg == "invalid integer: i123e3" test "deserialization to (ref) object": @@ -114,6 +124,7 @@ test "deserialization to JsonNode": test "list too long for array": let exception = - expect ValueError: + expect BencodeDecodeError: discard array[2, string].fromBencode("l5:hello5:world2:!!ee") + check exception.kind == WrongLength check exception.msg == "list too long: expected 2 items, got at least 3 items" From ed110a13b6a2b41d56837787f2c17eca8e64e2ca Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 22/38] Refactor encoding to use dump hooks --- bencode/decoding.nim | 2 +- bencode/encoding.nim | 59 ++++++++++++++++++++++---------------------- bencode/types.nim | 3 --- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 19dd309..1a8eb37 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -113,7 +113,7 @@ proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = consume(s, 'e') proc parseHook*[T](s: Stream; v: var OrderedTable[string, T]) = - # TODO why is this needed? + # why is this needed? parseHookTableImpl(s, v) proc parseHook*[T](s: Stream; v: var Table[string, T]) = diff --git a/bencode/encoding.nim b/bencode/encoding.nim index e41742b..1aa1358 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -5,38 +5,39 @@ import std/[ export types -proc bEncode*(obj: BencodeObj): string +proc dumpHook*(s: var string; v: string) = + s &= $v.len & ':' & v -proc encodeStr(s: string): string = - $s.len & ':' & s +proc dumpHook*(s: var string; v: int) = + s &= 'i' & $v & 'e' -proc encodeInt(i: int): string = - 'i' & $i & 'e' +proc dumpHook*[T](s: var string; v: openArray[T]) = + s &= "l" + for el in v: + dumpHook(s, el) + s &= "e" -proc encodeList(l: seq[BencodeObj]): string = - result = "l" - for el in l: - result &= bEncode(el) - result &= "e" - -proc encodeDict(d: OrderedTable[string, BencodeObj]): string = - var d = d - d.sort do (x, y: tuple[key: string; value: BencodeObj]) -> int: +proc dumpHook*[T](s: var string; v: OrderedTable[string, T]) = + var v = v + v.sort do (x, y: tuple[key: string; value: T]) -> int: system.cmp(x.key, y.key) - - result = "d" - for k, v in d.pairs(): - result &= encodeStr(k) & bEncode(v) - - result &= "e" + s &= "d" + for k, v in v.pairs(): + dumpHook(s, k) + dumpHook(s, v) + s &= "e" + +proc dumpHook*(s: var string; v: BencodeObj) = + case v.kind + of Str: + dumpHook(s, v.s) + of Int: + dumpHook(s, v.i) + of List: + dumpHook(s, v.l) + of Dict: + dumpHook(s, v.d) proc bEncode*(obj: BencodeObj): string = - result = case obj.kind - of Str: - encodeStr(obj.s) - of Int: - encodeInt(obj.i) - of List: - encodeList(obj.l) - of Dict: - encodeDict(obj.d) + result = "" + dumpHook(result, obj) diff --git a/bencode/types.nim b/bencode/types.nim index e2ca452..4454e47 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -183,6 +183,3 @@ func toBencodeObjImpl(value: NimNode): NimNode = macro toBencodeObj*(value: untyped): BencodeObj = toBencodeObjImpl(value) - -macro toBencode*(value: untyped): untyped {.deprecated: "use toBencodeObj instead".} = - toBencodeObjImpl(value) From 1bd780c9c141457882b033cd0e52bedc53441e22 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 23/38] Support encoding from object --- bencode/encoding.nim | 36 +++++++++++++++++--- bencode/private/utils.nim | 38 +++++++++++++++++++++ tests/tdecoding.nim | 62 --------------------------------- tests/troundtrip.nim | 72 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 bencode/private/utils.nim diff --git a/bencode/encoding.nim b/bencode/encoding.nim index 1aa1358..e76f39c 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -1,5 +1,8 @@ +import ./private/utils import ./types import std/[ + algorithm, + sequtils, tables, ] @@ -12,20 +15,30 @@ proc dumpHook*(s: var string; v: int) = s &= 'i' & $v & 'e' proc dumpHook*[T](s: var string; v: openArray[T]) = - s &= "l" + s &= 'l' for el in v: dumpHook(s, el) - s &= "e" + s &= 'e' proc dumpHook*[T](s: var string; v: OrderedTable[string, T]) = var v = v v.sort do (x, y: tuple[key: string; value: T]) -> int: system.cmp(x.key, y.key) - s &= "d" + s &= 'd' for k, v in v.pairs(): dumpHook(s, k) dumpHook(s, v) - s &= "e" + s &= 'e' + +proc dumpHook*[T](s: var string; v: Table[string, T]) = + var pairs = v.pairs.toSeq + pairs.sort do (a, b: (string, T)) -> int: + system.cmp(a[0], b[0]) + s &= 'd' + for (k, v) in pairs.items: + dumpHook(s, k) + dumpHook(s, v) + s &= 'e' proc dumpHook*(s: var string; v: BencodeObj) = case v.kind @@ -38,6 +51,21 @@ proc dumpHook*(s: var string; v: BencodeObj) = of Dict: dumpHook(s, v.d) +proc dumpHook*[T: object](s: var string; v: T) = + s &= 'd' + sortedFieldPairs(v, name, value): + dumpHook(s, name) + dumpHook(s, value) + s &= 'e' + +proc dumpHook*[T: ref object](s: var string; v: T) = + if v != nil: + dumpHook(s, v[]) + +proc toBencode*[T](v: T): string = + result = "" + dumpHook(result, v) + proc bEncode*(obj: BencodeObj): string = result = "" dumpHook(result, obj) diff --git a/bencode/private/utils.nim b/bencode/private/utils.nim new file mode 100644 index 0000000..36ecf4f --- /dev/null +++ b/bencode/private/utils.nim @@ -0,0 +1,38 @@ +import std/[ + algorithm, + macros, +] + +proc replaceIdents(body, nameIdent, valueIdent, ty, name: NimNode) = + nameIdent.expectKind nnkIdent + valueIdent.expectKind nnkIdent + ty.expectKind nnkSym + name.expectKind {nnkSym, nnkIdent} + + for i in 0 ..< body.len: + case body[i].kind + of nnkIdent: + if body[i].eqIdent(nameIdent): + body[i] = newLit(name.strVal) + elif body[i].eqIdent(valueIdent): + body[i] = newDotExpr(ty, name) + else: + replaceIdents(body[i], nameIdent, valueIdent, ty, name) + +macro sortedFieldPairs*(ty: object; nameIdent, valueIdent, body: untyped) = + if nameIdent.eqIdent(valueIdent): + error("names must be different", valueIdent) + result = newStmtList() + let objectTy = ty.getTypeImpl + objectTy.expectKind nnkObjectTy + let recList = objectTy[2] + recList.expectKind nnkRecList + var names = newSeq[NimNode]() + for son in recList: + son.expectKind nnkIdentDefs + names.add son[0] + names = names.sortedByIt(it.strVal) + for name in names: + let bodyCopy = body.copy + replaceIdents(bodyCopy, nameIdent, valueIdent, ty, name) + result.add bodyCopy diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 3cbfeba..d01e382 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -2,7 +2,6 @@ import ./utils import pkg/bencode/decoding import std/[ strutils, - tables, unittest, ] @@ -67,64 +66,3 @@ test "catch wrong dictionary key kind": discard bDecode(data) check exception.kind == SyntaxError check exception.msg == "invalid integer: i123e3" - -test "deserialization to (ref) object": - type - Record = object - name: string - lang: string - age: int - alist: seq[BencodeObj] - blist: seq[int] - mydict: OrderedTable[string, string] - myarray: array[2, string] - - let expectedRecord = Record( - name: "dmdm", - lang: "nim", - age: 50, - alist: @[Bencode(1), Bencode("hi")], - blist: @[100, 200], - mydict: {"foo": "bar"}.toOrderedTable, - myarray: ["hello", "world"] - ) - - const data = "d3:agei50e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" - check Record.fromBencode(data) == expectedRecord - let refRecord = (ref Record).fromBencode(data) - check refRecord != nil - check refRecord[] == expectedRecord - # test the nonsensical overload - {.push warning[Deprecated]:off.} - check data.fromBencode(Record) == expectedRecord - {.pop.} - -test "various table types": - const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" - let expectedTablePairs = { - "age": Bencode(50), - "alist": Bencode(@[Bencode(1), Bencode("hi")]), - "lang": Bencode("nim"), - "name": Bencode("dmdm"), - } - check OrderedTable[string, BencodeObj].fromBencode(data) == expectedTablePairs.toOrderedTable - check Table[string, BencodeObj].fromBencode(data) == expectedTablePairs.toTable - -import std/json - -test "deserialization to JsonNode": - const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" - let expected = %*{ - "age": 50, - "alist": [1, "hi"], - "lang": "nim", - "name": "dmdm", - } - check JsonNode.fromBencode(data) == expected - -test "list too long for array": - let exception = - expect BencodeDecodeError: - discard array[2, string].fromBencode("l5:hello5:world2:!!ee") - check exception.kind == WrongLength - check exception.msg == "list too long: expected 2 items, got at least 3 items" diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index 25e6051..1aca8df 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -1,3 +1,4 @@ +import ./utils import pkg/bencode/[ decoding, encoding, @@ -30,3 +31,74 @@ test "basic encode/decode": for k, v in testPairs.pairs: check bEncode(k) == v check bDecode(v) == k + +test "to/from (ref) object": + type + Record = object + name: string + lang: string + age: int + alist: seq[BencodeObj] + blist: seq[int] + mydict: OrderedTable[string, string] + myarray: array[2, string] + + let expectedRecord = Record( + name: "dmdm", + lang: "nim", + age: 50, + alist: @[Bencode(1), Bencode("hi")], + blist: @[100, 200], + mydict: {"foo": "bar"}.toOrderedTable, + myarray: ["hello", "world"] + ) + + # decode + const data = "d3:agei50e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" + check Record.fromBencode(data) == expectedRecord + let refRecord = (ref Record).fromBencode(data) + check refRecord != nil + check refRecord[] == expectedRecord + {.push warning[Deprecated]:off.} + check data.fromBencode(Record) == expectedRecord + {.pop.} + + # encode + const dataSorted = "d3:agei50e5:alistli1e2:hie5:blistli100ei200ee4:lang3:nim7:myarrayl5:hello5:worlde6:mydictd3:foo3:bare4:name4:dmdme" + check expectedRecord.toBencode == dataSorted + check refRecord.toBencode == dataSorted + +test "to/from various table types": + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + let expectedTablePairs = { + "age": Bencode(50), + "alist": Bencode(@[Bencode(1), Bencode("hi")]), + "lang": Bencode("nim"), + "name": Bencode("dmdm"), + } + check OrderedTable[string, BencodeObj].fromBencode(data) == expectedTablePairs.toOrderedTable + check Table[string, BencodeObj].fromBencode(data) == expectedTablePairs.toTable + check expectedTablePairs.toOrderedTable.toBencode == data + check expectedTablePairs.toTable.toBencode == data + +import std/json + +test "to JsonNode": + const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" + let expected = %*{ + "age": 50, + "alist": [1, "hi"], + "lang": "nim", + "name": "dmdm", + } + check JsonNode.fromBencode(data) == expected + +test "to/from array": + let exception = + expect BencodeDecodeError: + discard array[2, string].fromBencode("l5:hello5:world2:!!ee") + check exception.kind == WrongLength + check exception.msg == "list too long: expected 2 items, got at least 3 items" + + check array[2, string].fromBencode("l5:hello5:worlde") == ["hello", "world"] + check ["hello", "world"].toBencode == "l5:hello5:worlde" From 401d646d46ad2519bdf02803eb42d46359e23c38 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 24/38] Add back support for Nim 1.6 --- .github/workflows/test.yaml | 2 +- bencode.nim | 9 ++++----- bencode.nimble | 2 +- bencode/decoding.nim | 7 +++++-- bencode/encoding.nim | 2 +- bencode/private/{utils.nim => macros.nim} | 21 +++++++++++++++++++++ bencode/types.nim | 12 +++++++----- tests/treadme.nim | 3 ++- 8 files changed, 42 insertions(+), 16 deletions(-) rename bencode/private/{utils.nim => macros.nim} (65%) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 30caf3d..46a3769 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ jobs: matrix: nimVersion: - stable - - 2.0.8 + - 1.6.20 nimMm: - orc - arc diff --git a/bencode.nim b/bencode.nim index 45ec838..8dfb195 100644 --- a/bencode.nim +++ b/bencode.nim @@ -6,10 +6,9 @@ import ./bencode/[ export decoding, encoding when isMainModule: - import std/[ - os, - syncio, - ] + import std/os + when NimMajor >= 2: + import std/syncio proc parseFormatArg(arg: string): BencodeFormat = if arg.len < 2: quit("Invalid argument.") @@ -29,7 +28,7 @@ when isMainModule: block: let fnIdx = if paramStr(1)[0] == '-': 2 else: 1 (paramStr(fnIdx), paramStr(3 - fnIdx).parseFormatArg) - + let f = open(filename, fmRead) obj = bDecode(f) diff --git a/bencode.nimble b/bencode.nimble index ffc4347..16ae322 100644 --- a/bencode.nimble +++ b/bencode.nimble @@ -9,4 +9,4 @@ installExt = @["nim"] # Dependencies -requires "nim >= 2.0.8" +requires "nim >= 1.6.20" diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 1a8eb37..3c14eaf 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,12 +1,15 @@ import ./types import std/[ - assertions, parseutils, streams, strformat, - syncio, tables, ] +when NimMajor >= 2: + import std/[ + assertions, + syncio, + ] export types diff --git a/bencode/encoding.nim b/bencode/encoding.nim index e76f39c..1981ba2 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -1,4 +1,4 @@ -import ./private/utils +import ./private/macros import ./types import std/[ algorithm, diff --git a/bencode/private/utils.nim b/bencode/private/macros.nim similarity index 65% rename from bencode/private/utils.nim rename to bencode/private/macros.nim index 36ecf4f..4c90eb7 100644 --- a/bencode/private/utils.nim +++ b/bencode/private/macros.nim @@ -36,3 +36,24 @@ macro sortedFieldPairs*(ty: object; nameIdent, valueIdent, body: untyped) = let bodyCopy = body.copy replaceIdents(bodyCopy, nameIdent, valueIdent, ty, name) result.add bodyCopy + +proc removeDeprecatedImpl(body: NimNode) = + case body.kind + of nnkConstSection: + for son in body: + son.expectKind nnkConstDef + if son[0].kind == nnkPragmaExpr: + let pragma = son[0][1] + for i in countdown(pragma.len - 1, 0): + if pragma[i].kind == nnkExprColonExpr and pragma[i][0].eqIdent("deprecated"): + pragma.del(i) + else: + for son in body: + removeDeprecatedImpl(son) + +macro removeDeprecated*(body: untyped): untyped = + when NimMajor < 2: + result = body.copy + removeDeprecatedImpl(result) + else: + result = body diff --git a/bencode/types.nim b/bencode/types.nim index 4454e47..04bdeed 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -1,3 +1,4 @@ +import ./private/macros import std/[ enumerate, hashes, @@ -28,11 +29,12 @@ type Hexadecimal Decimal -const - bkStr* {.deprecated: "use Str instead".} = BencodeKind.Str - bkInt* {.deprecated: "use Int instead".} = BencodeKind.Int - bkList* {.deprecated: "use List instead".} = BencodeKind.List - bkDict* {.deprecated: "use Dict instead".} = BencodeKind.Dict +removeDeprecated: + const + bkStr* {.deprecated: "use Str instead".} = BencodeKind.Str + bkInt* {.deprecated: "use Int instead".} = BencodeKind.Int + bkList* {.deprecated: "use List instead".} = BencodeKind.List + bkDict* {.deprecated: "use Dict instead".} = BencodeKind.Dict # $ # diff --git a/tests/treadme.nim b/tests/treadme.nim index f9592e4..a83c9e8 100644 --- a/tests/treadme.nim +++ b/tests/treadme.nim @@ -1,5 +1,6 @@ import pkg/bencode -import std/assertions +when NimMajor >= 2: + import std/assertions let data = be({ From 5efba7b7eb1164f301fef51a8bd74ab1fa825b8d Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 25/38] Support decoding at compile time --- bencode/decoding.nim | 60 ++++++++++++++++++------------- bencode/private/streams.nim | 72 +++++++++++++++++++++++++++++++++++++ tests/tdecoding.nim | 9 +++-- tests/troundtrip.nim | 3 +- tests/utils.nim | 12 +++++++ 5 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 bencode/private/streams.nim diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 3c14eaf..e2923ab 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,7 +1,7 @@ +import ./private/streams import ./types import std/[ parseutils, - streams, strformat, tables, ] @@ -12,6 +12,7 @@ when NimMajor >= 2: ] export types +export atEnd, readChar, peekChar, getPosition, readStr # why is this needed? type BencodeDecodeErrorKind* = enum @@ -26,10 +27,10 @@ type proc newBencodeDecodeError(pos: int; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError = (ref BencodeDecodeError)(msg: msg, kind: kind, pos: pos) -proc newBencodeDecodeError(s: Stream; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError = +proc newBencodeDecodeError(s: var InputStream; kind: BencodeDecodeErrorKind; msg: string): ref BencodeDecodeError = newBencodeDecodeError(s.getPosition, kind, msg) -proc consume(s: Stream; c: char) = +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, UnexpectedEndOfInput, &"expected '{c}', got end of input") @@ -42,7 +43,7 @@ proc parseInt(str: string; pos: int): int = if parseutils.parseInt(str, result) != str.len: raise newBencodeDecodeError(pos, SyntaxError, &"invalid integer: {str}") -proc parseHook*(s: Stream; v: var string) = +proc parseHook*(s: var InputStream; v: var string) = # : # get the length var lengthStr = "" @@ -56,13 +57,14 @@ proc parseHook*(s: Stream; v: var string) = # read the string v = if length >= 0: - s.readStr(length) + try: + s.readStr(length) + except IOError as e: + raise newBencodeDecodeError(s, UnexpectedEndOfInput, e.msg) else: "" - if v.len != length: - raise newBencodeDecodeError(s, WrongLength, &"string too short: expected {length} characters, got {v.len} characters") -proc parseHook*(s: Stream; v: var int) = +proc parseHook*(s: var InputStream; v: var int) = # ie consume(s, 'i') var iStr = "" @@ -71,7 +73,7 @@ proc parseHook*(s: Stream; v: var int) = consume(s, 'e') v = parseInt(iStr, s.getPosition) -proc parseHook*[T](s: Stream; v: var seq[T]) = +proc parseHook*[T](s: var InputStream; v: var seq[T]) = # l ... e v = newSeq[T]() consume(s, 'l') @@ -81,7 +83,7 @@ proc parseHook*[T](s: Stream; v: var seq[T]) = v.add(item) consume(s, 'e') -proc parseHook*[T; C: static int](s: Stream; v: var array[C, T]) = +proc parseHook*[T; C: static int](s: var InputStream; v: var array[C, T]) = # l ... e v = default array[C, T] consume(s, 'l') @@ -97,7 +99,7 @@ proc parseHook*[T; C: static int](s: Stream; v: var array[C, T]) = type SomeTable[K, V] = Table[K, V] or OrderedTable[K, V] -proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = +proc parseHookTableImpl[T](s: var InputStream; v: var SomeTable[string, T]) = # d ... e var isReadingKey = true @@ -115,14 +117,14 @@ proc parseHookTableImpl[T](s: Stream; v: var SomeTable[string, T]) = # TODO raise on incomplete pair consume(s, 'e') -proc parseHook*[T](s: Stream; v: var OrderedTable[string, T]) = +proc parseHook*[T](s: var InputStream; v: var OrderedTable[string, T]) = # why is this needed? parseHookTableImpl(s, v) -proc parseHook*[T](s: Stream; v: var Table[string, T]) = +proc parseHook*[T](s: var InputStream; v: var Table[string, T]) = parseHookTableImpl(s, v) -proc parseHook*(s: Stream; v: var BencodeObj) = +proc parseHook*(s: var InputStream; v: var BencodeObj) = assert not s.atEnd case s.peekChar() of 'i': @@ -140,7 +142,7 @@ proc parseHook*(s: Stream; v: var BencodeObj) = import std/json -proc parseHook*(s: Stream; v: var JsonNode) = +proc parseHook*(s: var InputStream; v: var JsonNode) = assert not s.atEnd case s.peekChar() of 'i': @@ -162,7 +164,7 @@ proc parseHook*(s: Stream; v: var JsonNode) = parseHook(s, value) v = newJString(value) -proc parseHook*[T: object](s: Stream; v: var T) = +proc parseHook*[T: object](s: var InputStream; v: var T) = # d ... e # TODO Similar to the parseHook for OrderedTable. Unify or factor them somehow? var @@ -180,12 +182,11 @@ proc parseHook*[T: object](s: Stream; v: var T) = isReadingKey = true consume(s, 'e') -proc parseHook*[T: ref object](s: Stream; v: var T) = +proc parseHook*[T: ref object](s: var InputStream; v: var T) = v = T() parseHook(s, v[]) -proc fromBencode*(t: typedesc; s: Stream): t = - ## Decode bencoded data from `s` into a value of type `t`. +proc fromBencode(t: typedesc; s: var InputStream): t = result = default t parseHook(s, result) @@ -204,15 +205,23 @@ proc fromBencode*(t: typedesc; source: string): t = c: Bencode("embedded bencode"), ) - fromBencode(t, newStringStream(source)) + var s = toInputStream source + fromBencode(t, s) -proc fromBencode*(s: Stream; t: typedesc): t {.deprecated: "use fromBencode(typedesc, Stream) instead".} = - ## Logically backwards overload to match jsony's interface. - parseHook(s, result) +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, newStringStream(source)) + fromBencode(t, source) proc bDecode*(s: Stream): BencodeObj = ## Same as `BencodeObj.fromBencode(s)`. @@ -223,4 +232,5 @@ proc bDecode*(source: string): BencodeObj = fromBencode(BencodeObj, source) proc bDecode*(f: File): BencodeObj = - fromBencode(BencodeObj, newFileStream(f)) + ## Same as `BencodeObj.fromBencode(f)`. + fromBencode(BencodeObj, f) diff --git a/bencode/private/streams.nim b/bencode/private/streams.nim new file mode 100644 index 0000000..462e194 --- /dev/null +++ b/bencode/private/streams.nim @@ -0,0 +1,72 @@ +import std/streams + +type + InputStream* = concept v + atEnd(v) is bool + readChar(var v) is char + peekChar(v) is char + getPosition(v) is int + readStr(var v, int) is string + +# StringInputStream + +type + StringInputStream* = object + s: string + i: int + +proc toInputStream*(s: sink string): StringInputStream = + StringInputStream(s: s) + +proc atEnd*(s: StringInputStream): bool = + s.i >= s.s.len + +proc readChar*(s: var StringInputStream): char = + if s.atEnd: + raise (ref IOError)(msg: "no more chars to read") + result = s.s[s.i] + inc s.i + +proc peekChar*(s: StringInputStream): char = + if s.atEnd: + raise (ref IOError)(msg: "no more chars to peek") + result = s.s[s.i] + +proc getPosition*(s: StringInputStream): int = + s.i + +proc readStr*(s: var StringInputStream; len: int): string = + if s.i + len > s.s.len: + raise (ref IOError)(msg: "not enough data left to read a string of length " & $len) + result = s.s[s.i ..< s.i + len] + inc s.i, len + +# StreamInputStream + +type + StreamInputStream* = object + s: Stream + +proc toInputStream*(s: Stream): StreamInputStream = + StreamInputStream(s: s) + +proc atEnd*(s: StreamInputStream): bool = + s.s.atEnd + +proc readChar*(s: var StreamInputStream): char = + if s.s.atEnd: + raise (ref IOError)(msg: "no more chars to read") + result = s.s.readChar + +proc peekChar*(s: StreamInputStream): char = + if s.s.atEnd: + raise (ref IOError)(msg: "no more chars to peek") + result = s.s.peekChar + +proc getPosition*(s: StreamInputStream): int = + s.s.getPosition + +proc readStr*(s: var StreamInputStream; len: int): string = + result = s.s.readStr(len) + if result.len < len: + raise (ref IOError)(msg: "not enough data left to read a string of length " & $len) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index d01e382..2e7a4b5 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -1,6 +1,7 @@ import ./utils import pkg/bencode/decoding import std/[ + streams, strutils, unittest, ] @@ -21,8 +22,7 @@ test "string too short": let exception = expect BencodeDecodeError: discard bDecode("10:hello") - check exception.kind == WrongLength - check "string too short" in exception.msg + check exception.kind == UnexpectedEndOfInput test "invalid string length": let exception = @@ -51,7 +51,6 @@ test "unexpected end of input": expect BencodeDecodeError: discard bDecode("d5:hello5:world3:foo") check exception.kind == UnexpectedEndOfInput - check ExpectedMsg in exception.msg exception = expect BencodeDecodeError: @@ -66,3 +65,7 @@ test "catch wrong dictionary key kind": discard bDecode(data) check exception.kind == SyntaxError check exception.msg == "invalid integer: i123e3" + +test "various input types": + const expected = ["hello", "world", "!!"] + checkDecode(array[3, string], "l5:hello5:world2:!!ee", expected) diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index 1aca8df..f661cd9 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -4,6 +4,7 @@ import pkg/bencode/[ encoding, ] import std/[ + streams, tables, unittest, ] @@ -55,7 +56,7 @@ test "to/from (ref) object": # decode const data = "d3:agei50e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" - check Record.fromBencode(data) == expectedRecord + checkDecode(Record, data, expectedRecord) let refRecord = (ref Record).fromBencode(data) check refRecord != nil check refRecord[] == expectedRecord diff --git a/tests/utils.nim b/tests/utils.nim index 2148a6b..0611f11 100644 --- a/tests/utils.nim +++ b/tests/utils.nim @@ -9,3 +9,15 @@ template expect*[T: Exception](errorType: typedesc[T]; body: untyped): ref T = exception = e raise exception + +template checkDecode*(t: typedesc; data: static string; expected: untyped) = + # runtime string + block: + let d = data + check t.fromBencode(d) == expected + # compile-time string + block: + const actual = t.fromBencode(data) + check actual == expected + # stream + check t.fromBencode(newStringStream(data)) == expected From 815d09177f2320d9d1ca11c290b831215832e62a Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 26/38] Unify impls for Table and object parseHook --- bencode/decoding.nim | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index e2923ab..422431e 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -97,26 +97,30 @@ proc parseHook*[T; C: static int](s: var InputStream; v: var array[C, T]) = inc i 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]) = +template parseHookDictImpl(s: var InputStream; body: untyped) = # d ... e var isReadingKey = true - curKey = "" + curKey {.inject.} = "" consume(s, 'd') while not s.atEnd and s.peekChar() != 'e': if isReadingKey: parseHook(s, curKey) isReadingKey = false else: - var value = default T - parseHook(s, value) - v[curKey] = value + body isReadingKey = true # TODO raise on incomplete pair 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) @@ -165,22 +169,10 @@ proc parseHook*(s: var InputStream; v: var JsonNode) = v = newJString(value) proc parseHook*[T: object](s: var InputStream; v: var T) = - # d ... e - # TODO Similar to the parseHook for OrderedTable. Unify or factor them somehow? - var - isReadingKey = true - curKey = "" - consume(s, 'd') - while not s.atEnd and s.peekChar() != 'e': - if isReadingKey: - parseHook(s, curKey) - isReadingKey = false - else: - for name, value in fieldPairs(v): - if name == curKey: - parseHook(s, value) - isReadingKey = true - consume(s, 'e') + parseHookDictImpl(s): + for name, value in fieldPairs(v): + if name == curKey: + parseHook(s, value) proc parseHook*[T: ref object](s: var InputStream; v: var T) = v = T() From dca3e4b8392e3209d8454f210aa9f113d28a8e77 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 27/38] Add tool for extracting code blocks from readme --- tools/mdextractcodeblocks.nim | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tools/mdextractcodeblocks.nim diff --git a/tools/mdextractcodeblocks.nim b/tools/mdextractcodeblocks.nim new file mode 100644 index 0000000..f095752 --- /dev/null +++ b/tools/mdextractcodeblocks.nim @@ -0,0 +1,26 @@ +import std/[ + os, + strformat, + strutils, +] + +if paramCount() < 1: + quit "no filename given" +let + filename = paramStr(1) + name = filename.splitFile.name +var + f = File nil + i = 0 +for line in lines(filename): + if f == nil: + if line.strip == "```nim": + let outFilename = &"{name}_{i}.nim" + f = open(outFilename, fmWrite) + inc i + echo outFilename + else: + if line.strip == "```": + f.close + else: + f.writeLine(line) From f7b0f8740defd4cce1a61fc459a29896cef10b0c Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 28/38] Raise on incomplete dict pair --- bencode/decoding.nim | 8 ++++---- tests/tdecoding.nim | 29 ++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 422431e..63aa3bc 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -17,7 +17,6 @@ export atEnd, readChar, peekChar, getPosition, readStr # why is this needed? type BencodeDecodeErrorKind* = enum SyntaxError - UnexpectedEndOfInput WrongLength InvalidValue BencodeDecodeError* = object of ValueError @@ -33,7 +32,7 @@ proc newBencodeDecodeError(s: var InputStream; kind: BencodeDecodeErrorKind; 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, UnexpectedEndOfInput, &"expected '{c}', got end of input") + 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}") @@ -60,7 +59,7 @@ proc parseHook*(s: var InputStream; v: var string) = try: s.readStr(length) except IOError as e: - raise newBencodeDecodeError(s, UnexpectedEndOfInput, e.msg) + raise newBencodeDecodeError(s, SyntaxError, e.msg) else: "" @@ -110,7 +109,8 @@ template parseHookDictImpl(s: var InputStream; body: untyped) = else: body isReadingKey = true - # TODO raise on incomplete pair + 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] diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 2e7a4b5..2b4f877 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -22,7 +22,7 @@ test "string too short": let exception = expect BencodeDecodeError: discard bDecode("10:hello") - check exception.kind == UnexpectedEndOfInput + check exception.kind == SyntaxError test "invalid string length": let exception = @@ -32,30 +32,41 @@ test "invalid string length": check "invalid string length" in exception.msg test "unexpected end of input": - const ExpectedMsg = "expected 'e'" - var exception: ref BencodeDecodeError exception = expect BencodeDecodeError: discard bDecode("l") - check exception.kind == UnexpectedEndOfInput - check ExpectedMsg in exception.msg + check exception.kind == SyntaxError + check "expected 'e'" in exception.msg exception = expect BencodeDecodeError: discard bDecode("d") - check exception.kind == UnexpectedEndOfInput - check ExpectedMsg in exception.msg + check exception.kind == SyntaxError + check "expected 'e'" in exception.msg + + exception = + expect BencodeDecodeError: + discard bDecode("d5:hello5:world3:foo3:bar") + check exception.kind == SyntaxError + check "expected 'e'" in exception.msg exception = expect BencodeDecodeError: discard bDecode("d5:hello5:world3:foo") - check exception.kind == UnexpectedEndOfInput + check exception.kind == SyntaxError + check "expected value" in exception.msg + + exception = + expect BencodeDecodeError: + discard bDecode("d5:hello5:world3:fooe") + check exception.kind == SyntaxError + check "expected value" in exception.msg exception = expect BencodeDecodeError: echo bDecode("5") - check exception.kind == UnexpectedEndOfInput + check exception.kind == SyntaxError check "expected ':'" in exception.msg test "catch wrong dictionary key kind": From f37828aff9fcd489688646e6dd14bb86169320c6 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 29/38] Docs --- bencode/decoding.nim | 14 +++++++++----- bencode/encoding.nim | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 63aa3bc..502ec6a 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -185,16 +185,20 @@ proc fromBencode(t: typedesc; s: var InputStream): t = 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:c16:embedded bencodee" + 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("embedded bencode"), + c: Bencode("bencode"), + d: %*{"works": "with JsonNode too"}, ) var s = toInputStream source @@ -216,13 +220,13 @@ proc fromBencode*(source: string; t: typedesc): t {.deprecated: "use fromBencode fromBencode(t, source) proc bDecode*(s: Stream): BencodeObj = - ## Same as `BencodeObj.fromBencode(s)`. + ## Same as `BencodeObj.fromBencode(s)<#fromBencode,typedesc,Stream>`_. fromBencode(BencodeObj, s) proc bDecode*(source: string): BencodeObj = - ## Same as `BencodeObj.fromBencode(source)`. + ## Same as `BencodeObj.fromBencode(source)<#fromBencode,typedesc,string>`_. fromBencode(BencodeObj, source) proc bDecode*(f: File): BencodeObj = - ## Same as `BencodeObj.fromBencode(f)`. + ## Same as `BencodeObj.fromBencode(f)<#fromBencode,typedesc,File>`_. fromBencode(BencodeObj, f) diff --git a/bencode/encoding.nim b/bencode/encoding.nim index 1981ba2..84ac86f 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -63,9 +63,26 @@ proc dumpHook*[T: ref object](s: var string; v: T) = dumpHook(s, v[]) proc toBencode*[T](v: T): string = + ## Encode `v` as bencode. + ## + ## .. Note:: The macro that used to be called `toBencode` is now `toBencodeObj`. + runnableExamples: + type Record = object + name: string + data: BencodeObj + + let record = Record( + name: "Steve", + data: be({ + "foo": be"bar", + "baz": be(1), + }), + ) + doAssert record.toBencode == "d4:datad3:bazi1e3:foo3:bare4:name5:Stevee" + result = "" dumpHook(result, v) proc bEncode*(obj: BencodeObj): string = - result = "" - dumpHook(result, obj) + ## Same as `obj.toBencode<#toBencode,T>`_. + toBencode(obj) From 39c6c664e13659ec5cd82fa1d3a3dce584f0fd97 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 30/38] Fix incorrect decoding when key missing in object --- bencode/decoding.nim | 11 ++++++++--- tests/tdecoding.nim | 3 +++ tests/troundtrip.nim | 12 +++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 502ec6a..28cecbb 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -170,9 +170,14 @@ proc parseHook*(s: var InputStream; v: var JsonNode) = proc parseHook*[T: object](s: var InputStream; v: var T) = parseHookDictImpl(s): - for name, value in fieldPairs(v): - if name == curKey: - parseHook(s, value) + block outer: + for name, value in fieldPairs(v): + if name == curKey: + parseHook(s, value) + 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() diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 2b4f877..05414af 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -80,3 +80,6 @@ test "catch wrong dictionary key kind": test "various input types": const expected = ["hello", "world", "!!"] checkDecode(array[3, string], "l5:hello5:world2:!!ee", expected) + +test "zero-length string": + checkDecode(string, "0:", "") diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index f661cd9..82a1de5 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -43,6 +43,7 @@ test "to/from (ref) object": blist: seq[int] mydict: OrderedTable[string, string] myarray: array[2, string] + notInBencode: string let expectedRecord = Record( name: "dmdm", @@ -51,11 +52,12 @@ test "to/from (ref) object": alist: @[Bencode(1), Bencode("hi")], blist: @[100, 200], mydict: {"foo": "bar"}.toOrderedTable, - myarray: ["hello", "world"] + myarray: ["hello", "world"], + notInBencode: "", ) # decode - const data = "d3:agei50e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" + const data = "d3:agei50e9:extra keyi123e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" checkDecode(Record, data, expectedRecord) let refRecord = (ref Record).fromBencode(data) check refRecord != nil @@ -65,9 +67,9 @@ test "to/from (ref) object": {.pop.} # encode - const dataSorted = "d3:agei50e5:alistli1e2:hie5:blistli100ei200ee4:lang3:nim7:myarrayl5:hello5:worlde6:mydictd3:foo3:bare4:name4:dmdme" - check expectedRecord.toBencode == dataSorted - check refRecord.toBencode == dataSorted + const dataOut = "d3:agei50e5:alistli1e2:hie5:blistli100ei200ee4:lang3:nim7:myarrayl5:hello5:worlde6:mydictd3:foo3:bare4:name4:dmdm12:notInBencode0:e" + check expectedRecord.toBencode == dataOut + check refRecord.toBencode == dataOut test "to/from various table types": const data = "d3:agei50e5:alistli1e2:hie4:lang3:nim4:name4:dmdme" From e9039cc533e6d8829ea644563c61aaf3cdb330f3 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 31/38] Support encoding from JsonNode --- bencode/decoding.nim | 2 +- bencode/encoding.nim | 31 ++++++++++++++++++++++++++++++- bencode/private/macros.nim | 9 +++++++-- bencode/types.nim | 1 + tests/tencoding.nim | 12 ++++++++++++ tests/tencoding_dumphook.nim | 11 +++++++++++ 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 tests/tencoding.nim create mode 100644 tests/tencoding_dumphook.nim diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 28cecbb..8a2eabd 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -19,7 +19,7 @@ type SyntaxError WrongLength InvalidValue - BencodeDecodeError* = object of ValueError + BencodeDecodeError* = object of BencodeError kind* {.requiresInit.}: BencodeDecodeErrorKind pos* {.requiresInit.}: int diff --git a/bencode/encoding.nim b/bencode/encoding.nim index 84ac86f..cff9ca6 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -8,6 +8,9 @@ import std/[ export types +type + BencodeEncodeError* = object of BencodeError + proc dumpHook*(s: var string; v: string) = s &= $v.len & ':' & v @@ -51,6 +54,28 @@ proc dumpHook*(s: var string; v: BencodeObj) = of Dict: dumpHook(s, v.d) +import std/json + +proc dumpHook*[T: JsonNode](s: var string; v: T) = + case v.kind + of JString: + dumpHook(s, v.getStr) + of JInt: + dumpHook(s, v.getInt) + of JArray: + dumpHook(s, v.getElems) + of JObject: + dumpHook(s, v.getFields) + of JNull: + raise (ref BencodeEncodeError)(msg: "cannot bencode JSON null") + of JFloat: + when compiles(dumpHook(s, v.getFloat)): + dumpHook(s, v.getFloat) + else: + raise (ref BencodeEncodeError)(msg: "cannot bencode JSON float") + of JBool: + dumpHook(s, v.getBool.int) + proc dumpHook*[T: object](s: var string; v: T) = s &= 'd' sortedFieldPairs(v, name, value): @@ -67,9 +92,12 @@ proc toBencode*[T](v: T): string = ## ## .. Note:: The macro that used to be called `toBencode` is now `toBencodeObj`. runnableExamples: + import std/json + type Record = object name: string data: BencodeObj + json: JsonNode let record = Record( name: "Steve", @@ -77,8 +105,9 @@ proc toBencode*[T](v: T): string = "foo": be"bar", "baz": be(1), }), + json: %*{"hello": "world"}, ) - doAssert record.toBencode == "d4:datad3:bazi1e3:foo3:bare4:name5:Stevee" + doAssert record.toBencode == "d4:datad3:bazi1e3:foo3:bare4:jsond5:hello5:worlde4:name5:Stevee" result = "" dumpHook(result, v) diff --git a/bencode/private/macros.nim b/bencode/private/macros.nim index 4c90eb7..4e080cf 100644 --- a/bencode/private/macros.nim +++ b/bencode/private/macros.nim @@ -29,8 +29,13 @@ macro sortedFieldPairs*(ty: object; nameIdent, valueIdent, body: untyped) = recList.expectKind nnkRecList var names = newSeq[NimNode]() for son in recList: - son.expectKind nnkIdentDefs - names.add son[0] + case son.kind + of nnkIdentDefs: + names.add son[0] + of nnkRecCase: + error("object variants are not supported", son) + else: + error("unsupported node kind", son) names = names.sortedByIt(it.strVal) for name in names: let bodyCopy = body.copy diff --git a/bencode/types.nim b/bencode/types.nim index 04bdeed..5fd9586 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -28,6 +28,7 @@ type Normal Hexadecimal Decimal + BencodeError* = object of ValueError removeDeprecated: const diff --git a/tests/tencoding.nim b/tests/tencoding.nim new file mode 100644 index 0000000..d7cdf5f --- /dev/null +++ b/tests/tencoding.nim @@ -0,0 +1,12 @@ +import pkg/bencode/encoding +import std/[ + json, + unittest, +] + +test "from JsonNode": + let j = %*{"foo": "bar", "baz": [1, "qux", true]} + check j.toBencode == "d3:bazli1e3:quxi1ee3:foo3:bare" + + expect BencodeEncodeError: + discard (%*{"wow": 3.14}).toBencode diff --git a/tests/tencoding_dumphook.nim b/tests/tencoding_dumphook.nim new file mode 100644 index 0000000..2c7e1f8 --- /dev/null +++ b/tests/tencoding_dumphook.nim @@ -0,0 +1,11 @@ +import pkg/bencode/encoding +import std/[ + json, + unittest, +] + +proc dumpHook*(s: var string; v: float) = + dumpHook(s, int(v * 100)) + +test "from JsonNode with float and custom dumpHook": + check (%*{"wow": 3.14}).toBencode == "d3:wowi314ee" From f9948a6600ccaee15df1fd6b669cd006d90c636e Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 32/38] Fix encoding nils --- bencode/encoding.nim | 28 ++++++++++++++++++---------- tests/troundtrip.nim | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/bencode/encoding.nim b/bencode/encoding.nim index cff9ca6..261d5c8 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -23,14 +23,23 @@ proc dumpHook*[T](s: var string; v: openArray[T]) = dumpHook(s, el) s &= 'e' +template maybeDumpDictPair(s: var string; k, v: untyped) = + # TODO skipHook for values + when v is ref: + if v != nil: + dumpHook(s, k) + dumpHook(s, v) + else: + dumpHook(s, k) + dumpHook(s, v) + proc dumpHook*[T](s: var string; v: OrderedTable[string, T]) = var v = v v.sort do (x, y: tuple[key: string; value: T]) -> int: system.cmp(x.key, y.key) s &= 'd' for k, v in v.pairs(): - dumpHook(s, k) - dumpHook(s, v) + maybeDumpDictPair(s, k, v) s &= 'e' proc dumpHook*[T](s: var string; v: Table[string, T]) = @@ -39,8 +48,7 @@ proc dumpHook*[T](s: var string; v: Table[string, T]) = system.cmp(a[0], b[0]) s &= 'd' for (k, v) in pairs.items: - dumpHook(s, k) - dumpHook(s, v) + maybeDumpDictPair(s, k, v) s &= 'e' proc dumpHook*(s: var string; v: BencodeObj) = @@ -67,25 +75,25 @@ proc dumpHook*[T: JsonNode](s: var string; v: T) = of JObject: dumpHook(s, v.getFields) of JNull: - raise (ref BencodeEncodeError)(msg: "cannot bencode JSON null") + raise (ref BencodeEncodeError)(msg: "cannot bencode a JSON null") of JFloat: when compiles(dumpHook(s, v.getFloat)): dumpHook(s, v.getFloat) else: - raise (ref BencodeEncodeError)(msg: "cannot bencode JSON float") + raise (ref BencodeEncodeError)(msg: "cannot bencode a JSON float") of JBool: dumpHook(s, v.getBool.int) proc dumpHook*[T: object](s: var string; v: T) = s &= 'd' sortedFieldPairs(v, name, value): - dumpHook(s, name) - dumpHook(s, value) + maybeDumpDictPair(s, name, value) s &= 'e' proc dumpHook*[T: ref object](s: var string; v: T) = - if v != nil: - dumpHook(s, v[]) + if v == nil: + raise (ref BencodeEncodeError)(msg: "cannot bencode a nil ref") + dumpHook(s, v[]) proc toBencode*[T](v: T): string = ## Encode `v` as bencode. diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index 82a1de5..60d8dde 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -105,3 +105,18 @@ test "to/from array": check array[2, string].fromBencode("l5:hello5:worlde") == ["hello", "world"] check ["hello", "world"].toBencode == "l5:hello5:worlde" + +test "nil ref objects": + type + Foo = object + s: string + Bar = object + e: int + f: ref Foo + g: string + + let bar = Bar(e: 321, f: nil, g: "hi") + const expected = "d1:ei321e1:g2:hie" + let actual = bar.toBencode + check actual == expected + check Bar.fromBencode(actual) == bar From e608abe115e68f6836fcb8570e285e4d9da3db5c Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:39:51 +0000 Subject: [PATCH 33/38] Export InputStream --- bencode/decoding.nim | 9 +++++--- .../{private/streams.nim => inputstreams.nim} | 0 tests/tdecoding_parsehook.nim | 23 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) rename bencode/{private/streams.nim => inputstreams.nim} (100%) create mode 100644 tests/tdecoding_parsehook.nim diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 8a2eabd..2cb02a6 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,5 +1,7 @@ -import ./private/streams -import ./types +import ./[ + inputstreams, + types, +] import std/[ parseutils, strformat, @@ -11,6 +13,7 @@ when NimMajor >= 2: syncio, ] +export InputStream export types export atEnd, readChar, peekChar, getPosition, readStr # why is this needed? @@ -38,7 +41,7 @@ proc consume(s: var InputStream; c: char) = raise newBencodeDecodeError(s, SyntaxError, &"expected '{c}', got {actual}") proc parseInt(str: string; pos: int): int = - result = 0 # parseutils.parseInt's second parameter really should be marked `out` + result = 0 if parseutils.parseInt(str, result) != str.len: raise newBencodeDecodeError(pos, SyntaxError, &"invalid integer: {str}") diff --git a/bencode/private/streams.nim b/bencode/inputstreams.nim similarity index 100% rename from bencode/private/streams.nim rename to bencode/inputstreams.nim diff --git a/tests/tdecoding_parsehook.nim b/tests/tdecoding_parsehook.nim new file mode 100644 index 0000000..a9907bf --- /dev/null +++ b/tests/tdecoding_parsehook.nim @@ -0,0 +1,23 @@ +import pkg/bencode/decoding +import std/[ + times, + unittest, +] + +proc parseHook*(s: var InputStream; v: var DateTime) = + var str = "" + parseHook(s, str) + v = times.parse(str, "yyyy-MM-dd'T'HH:mm:ssz", utc()) + +test "custom parseHook": + type + Foo = object + c: int + date: DateTime + e: string + + check Foo.fromBencode("d1:ci42e4:date20:2020-01-01T10:20:30Z1:e2:hie") == Foo( + c: 42, + date: dateTime(2020, mJan, 1, 10, 20, 30, zone = utc()), + e: "hi", + ) From 852cdb7bc75622d24843cfb7a501b522d71799aa Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 13:49:53 +0800 Subject: [PATCH 34/38] Trigger test on pull requests --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 46a3769..3854e67 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,6 +3,7 @@ on: push: branches: - master + pull_request: workflow_dispatch: jobs: test: From 526c7e1a31054f1985787023f9c2b8b01e7e1124 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:13:15 +0800 Subject: [PATCH 35/38] Update docs --- bencode/encoding.nim | 2 +- bencode/types.nim | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bencode/encoding.nim b/bencode/encoding.nim index 261d5c8..df637e2 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -98,7 +98,7 @@ proc dumpHook*[T: ref object](s: var string; v: T) = proc toBencode*[T](v: T): string = ## Encode `v` as bencode. ## - ## .. Note:: The macro that used to be called `toBencode` is now `toBencodeObj`. + ## .. Note:: The macro that used to be called `toBencode` is now `toBencodeObj`_. Consider instead encoding directly from an object as shown below. runnableExamples: import std/json diff --git a/bencode/types.nim b/bencode/types.nim index 5fd9586..77d092b 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -185,4 +185,5 @@ func toBencodeObjImpl(value: NimNode): NimNode = newCall(bindSym("Bencode", brOpen), value) macro toBencodeObj*(value: untyped): BencodeObj = + ## .. Note:: Consider instead encoding directly from an object using `toBencode`_. toBencodeObjImpl(value) From b19a7d8badce975901fec7411fe52b0f90006c0a Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:13:22 +0800 Subject: [PATCH 36/38] Update tests --- tests/tdecoding.nim | 2 +- tests/tdecoding_parsehook.nim | 2 +- tests/tencoding.nim | 3 ++- tests/tencoding_dumphook.nim | 2 +- tests/troundtrip.nim | 5 +---- tests/ttypes.nim | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/tdecoding.nim b/tests/tdecoding.nim index 05414af..6ce78c7 100644 --- a/tests/tdecoding.nim +++ b/tests/tdecoding.nim @@ -1,5 +1,5 @@ import ./utils -import pkg/bencode/decoding +import pkg/bencode import std/[ streams, strutils, diff --git a/tests/tdecoding_parsehook.nim b/tests/tdecoding_parsehook.nim index a9907bf..c232ae8 100644 --- a/tests/tdecoding_parsehook.nim +++ b/tests/tdecoding_parsehook.nim @@ -1,4 +1,4 @@ -import pkg/bencode/decoding +import pkg/bencode import std/[ times, unittest, diff --git a/tests/tencoding.nim b/tests/tencoding.nim index d7cdf5f..7fe2813 100644 --- a/tests/tencoding.nim +++ b/tests/tencoding.nim @@ -1,4 +1,4 @@ -import pkg/bencode/encoding +import pkg/bencode import std/[ json, unittest, @@ -7,6 +7,7 @@ import std/[ test "from JsonNode": let j = %*{"foo": "bar", "baz": [1, "qux", true]} check j.toBencode == "d3:bazli1e3:quxi1ee3:foo3:bare" + check toBencode(j) == "d3:bazli1e3:quxi1ee3:foo3:bare" expect BencodeEncodeError: discard (%*{"wow": 3.14}).toBencode diff --git a/tests/tencoding_dumphook.nim b/tests/tencoding_dumphook.nim index 2c7e1f8..2396d50 100644 --- a/tests/tencoding_dumphook.nim +++ b/tests/tencoding_dumphook.nim @@ -1,4 +1,4 @@ -import pkg/bencode/encoding +import pkg/bencode import std/[ json, unittest, diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index 60d8dde..aaf7ac7 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -1,8 +1,5 @@ import ./utils -import pkg/bencode/[ - decoding, - encoding, -] +import pkg/bencode import std/[ streams, tables, diff --git a/tests/ttypes.nim b/tests/ttypes.nim index edb3e5c..534f1c0 100644 --- a/tests/ttypes.nim +++ b/tests/ttypes.nim @@ -1,4 +1,4 @@ -import pkg/bencode/types +import pkg/bencode import std/[ tables, unittest, From 722b5cd405b10a8ac008a38ae3825546eeded6bf Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:51:43 +0800 Subject: [PATCH 37/38] Allow custom encoded name --- README.md | 54 ++++++++++++++++++++++- bencode/decoding.nim | 7 +-- bencode/encoding.nim | 4 +- bencode/private/macros.nim | 52 +++++++++++++--------- bencode/types.nim | 2 + tests/toldreadme.nim | 16 +++++++ tests/treadme.nim | 83 ++++++++++++++++++++++++++++------- tests/troundtrip.nim | 6 +-- tools/mdextractcodeblocks.nim | 1 + 9 files changed, 180 insertions(+), 45 deletions(-) create mode 100644 tests/toldreadme.nim diff --git a/README.md b/README.md index fa735ae..cf27d2e 100644 --- a/README.md +++ b/README.md @@ -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), @@ -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 ``` diff --git a/bencode/decoding.nim b/bencode/decoding.nim index 2cb02a6..c80c076 100644 --- a/bencode/decoding.nim +++ b/bencode/decoding.nim @@ -1,3 +1,4 @@ +import ./private/macros import ./[ inputstreams, types, @@ -174,9 +175,9 @@ proc parseHook*(s: var InputStream; v: var JsonNode) = proc parseHook*[T: object](s: var InputStream; v: var T) = parseHookDictImpl(s): block outer: - for name, value in fieldPairs(v): - if name == curKey: - parseHook(s, value) + 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() diff --git a/bencode/encoding.nim b/bencode/encoding.nim index df637e2..bed919a 100644 --- a/bencode/encoding.nim +++ b/bencode/encoding.nim @@ -86,8 +86,8 @@ proc dumpHook*[T: JsonNode](s: var string; v: T) = proc dumpHook*[T: object](s: var string; v: T) = s &= 'd' - sortedFieldPairs(v, name, value): - maybeDumpDictPair(s, name, value) + for fieldName, fieldValue in sortedFieldPairs(v): + maybeDumpDictPair(s, effectiveName(fieldName, fieldValue), fieldValue) s &= 'e' proc dumpHook*[T: ref object](s: var string; v: T) = diff --git a/bencode/private/macros.nim b/bencode/private/macros.nim index 4e080cf..a7ca8f1 100644 --- a/bencode/private/macros.nim +++ b/bencode/private/macros.nim @@ -3,30 +3,17 @@ import std/[ macros, ] -proc replaceIdents(body, nameIdent, valueIdent, ty, name: NimNode) = - nameIdent.expectKind nnkIdent - valueIdent.expectKind nnkIdent - ty.expectKind nnkSym - name.expectKind {nnkSym, nnkIdent} - - for i in 0 ..< body.len: - case body[i].kind - of nnkIdent: - if body[i].eqIdent(nameIdent): - body[i] = newLit(name.strVal) - elif body[i].eqIdent(valueIdent): - body[i] = newDotExpr(ty, name) - else: - replaceIdents(body[i], nameIdent, valueIdent, ty, name) - -macro sortedFieldPairs*(ty: object; nameIdent, valueIdent, body: untyped) = +macro sortedFieldPairsImpl(ty: object; nameIdent, valueIdent, body: untyped) = + body.expectKind nnkStmtList if nameIdent.eqIdent(valueIdent): error("names must be different", valueIdent) + result = newStmtList() let objectTy = ty.getTypeImpl objectTy.expectKind nnkObjectTy let recList = objectTy[2] recList.expectKind nnkRecList + var names = newSeq[NimNode]() for son in recList: case son.kind @@ -37,10 +24,27 @@ macro sortedFieldPairs*(ty: object; nameIdent, valueIdent, body: untyped) = else: error("unsupported node kind", son) names = names.sortedByIt(it.strVal) + for name in names: - let bodyCopy = body.copy - replaceIdents(bodyCopy, nameIdent, valueIdent, ty, name) - result.add bodyCopy + let newBody = newStmtList( + newProc(nameIdent, [bindSym"untyped"], newLit(name.strVal), nnkTemplateDef, nnkPragma.newTree(ident"used")), + newProc(valueIdent, [bindSym"untyped"], newDotExpr(ty, name), nnkTemplateDef, nnkPragma.newTree(ident"used")), + ) + for node in body: + newBody.add node + result.add newBlockStmt(newBody) + +macro sortedFieldPairs*(loop: ForLoopStmt) = + if loop.len != 4: + error("wrong number of arguments") + let + nameIdent = loop[0] + valueIdent = loop[1] + call = loop[2] + body = loop[3] + call.expectKind nnkCall + let tyIdent = call[1] + result = newCall(bindSym"sortedFieldPairsImpl", tyIdent, nameIdent, valueIdent, body) proc removeDeprecatedImpl(body: NimNode) = case body.kind @@ -62,3 +66,11 @@ macro removeDeprecated*(body: untyped): untyped = removeDeprecatedImpl(result) else: result = body + +import ../types + +template effectiveName*(fieldName, fieldValue: untyped): untyped = + when fieldValue.hasCustomPragma(types.name): + fieldValue.getCustomPragmaVal(types.name) + else: + fieldName diff --git a/bencode/types.nim b/bencode/types.nim index 77d092b..4c4f765 100644 --- a/bencode/types.nim +++ b/bencode/types.nim @@ -8,6 +8,8 @@ import std/[ tables, ] +template name*(name: string) {.pragma.} + type BencodeKind* = enum Str = "string" diff --git a/tests/toldreadme.nim b/tests/toldreadme.nim new file mode 100644 index 0000000..a83c9e8 --- /dev/null +++ b/tests/toldreadme.nim @@ -0,0 +1,16 @@ +import pkg/bencode +when NimMajor >= 2: + import std/assertions + +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 = bEncode(data) + +doAssert bencodedData == "d8:completei20e10:incompletei0e8:intervali1800e12:min intervali900e5:peers6:\x0a\x0a\x0a\x05\x00\x80e" +doAssert bDecode(bencodedData) == data diff --git a/tests/treadme.nim b/tests/treadme.nim index a83c9e8..a5ce0df 100644 --- a/tests/treadme.nim +++ b/tests/treadme.nim @@ -1,16 +1,69 @@ import pkg/bencode -when NimMajor >= 2: - import std/assertions - -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 = bEncode(data) - -doAssert bencodedData == "d8:completei20e10:incompletei0e8:intervali1800e12:min intervali900e5:peers6:\x0a\x0a\x0a\x05\x00\x80e" -doAssert bDecode(bencodedData) == data + +const Expected = "d8:completei20e10:incompletei0e8:intervali1800e12:min intervali900e5:peers6:\x0a\x0a\x0a\x05\x00\x80e" + +block: + 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 + +import std/tables + +block: + let + data = { + "interval": be(1800), + "min interval": be(900), + "peers": be("\x0a\x0a\x0a\x05\x00\x80"), + "complete": be(20), + "incomplete": be(0), + }.toTable + bencodedData = data.toBencode() + + doAssert bencodedData == Expected + doAssert Table[string, BencodeObj].fromBencode(bencodedData) == data + +block: + 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 + +block: + 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 = bEncode(data) + + doAssert bencodedData == Expected + doAssert bDecode(bencodedData) == data diff --git a/tests/troundtrip.nim b/tests/troundtrip.nim index aaf7ac7..b11e1bf 100644 --- a/tests/troundtrip.nim +++ b/tests/troundtrip.nim @@ -34,7 +34,7 @@ test "to/from (ref) object": type Record = object name: string - lang: string + lang {.name: "the language".}: string age: int alist: seq[BencodeObj] blist: seq[int] @@ -54,7 +54,7 @@ test "to/from (ref) object": ) # decode - const data = "d3:agei50e9:extra keyi123e7:myarrayl5:hello5:worlde5:alistli1e2:hie4:lang3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" + const data = "d3:agei50e9:extra keyi123e7:myarrayl5:hello5:worlde5:alistli1e2:hie12:the language3:nim4:name4:dmdm5:blistli100ei200ee6:mydictd3:foo3:baree" checkDecode(Record, data, expectedRecord) let refRecord = (ref Record).fromBencode(data) check refRecord != nil @@ -64,7 +64,7 @@ test "to/from (ref) object": {.pop.} # encode - const dataOut = "d3:agei50e5:alistli1e2:hie5:blistli100ei200ee4:lang3:nim7:myarrayl5:hello5:worlde6:mydictd3:foo3:bare4:name4:dmdm12:notInBencode0:e" + const dataOut = "d3:agei50e5:alistli1e2:hie5:blistli100ei200ee12:the language3:nim7:myarrayl5:hello5:worlde6:mydictd3:foo3:bare4:name4:dmdm12:notInBencode0:e" check expectedRecord.toBencode == dataOut check refRecord.toBencode == dataOut diff --git a/tools/mdextractcodeblocks.nim b/tools/mdextractcodeblocks.nim index f095752..a9b6b7c 100644 --- a/tools/mdextractcodeblocks.nim +++ b/tools/mdextractcodeblocks.nim @@ -22,5 +22,6 @@ for line in lines(filename): else: if line.strip == "```": f.close + f = nil else: f.writeLine(line) From 1e184cd65f3b917c1496df3e6e4cd2d4aaa44a66 Mon Sep 17 00:00:00 2001 From: Zack Guard <5311230+z-------------@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:53:39 +0800 Subject: [PATCH 38/38] Use setup-nim-action v2 --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3854e67..40e2d1e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,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 }}