app "roctorrent" packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } imports [pf.Stdout] provides [main] to pf # Bencoded strings are not character strings i.e. text but binary data Bytes: (List U8) # Decodes an exact string, errors if there is remaining unused data bDecodeStr: Bytes -> Result (Bytes) [Malformatted Str] bDecodeStr = \i -> when bDecodeStrAndRem i is Ok {res, others} -> if List.len others == 0 then Ok res else Err (Malformatted "bDecodeStr: trailing characters") Err (Malformatted s) -> Err (Malformatted "bDecodeStr: error calling bDecodeStrAndRem \(s)") # Decodes a string and returns the remainder of the input as well if there was any bDecodeStrAndRem: Bytes -> Result {res: Bytes, others: Bytes} [Malformatted Str] bDecodeStrAndRem = \i -> when (List.splitFirst i ':') is Ok {before, after} -> when (Str.fromUtf8 before) is Ok ls -> when (Str.toNat ls) is Ok l -> if List.len after >= l then r2 = (List.split after l) Ok {res: r2.before, others: r2.others} else Err (Malformatted "bDecodeStrAndRem: len exceeds remaining data") Err _err -> Err (Malformatted "bDecodeStrAndRem: failed to parse number") Err _err -> Err (Malformatted "bDecodeStrAndRem: failed to parse utf8") Err _err -> Err (Malformatted "bDecodeStrAndRem: no ':' found") expect res = bDecodeStr (Str.toUtf8 "3:foo") res == Ok (Str.toUtf8 "foo") expect errCases = [ {case: "", err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: no ':' found"}, {case: "foo", err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: no ':' found"}, {case: "1:foo", err: "bDecodeStr: trailing characters"}, {case: "4:foo",err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: len exceeds remaining data"}, {case: ":", err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: failed to parse number"}, {case: "foo:", err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: failed to parse number"}, {case: "1:",err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: len exceeds remaining data"}, {case: "::",err: "bDecodeStr: error calling bDecodeStrAndRem bDecodeStrAndRem: failed to parse number"}, ] results = List.map errCases \t -> when bDecodeStr (Str.toUtf8 t.case) is Ok _ -> "Malformatted input \(t.case) should have failed!" Err (Malformatted err) -> err expected = List.map errCases \t -> t.err results == expected # Decodes an exact number, exta input causes an error bDecodeNum: Bytes -> Result I64 [Malformatted Str] bDecodeNum = \i -> when bDecodeNumAndRem i is Ok {num, rem} -> if (List.len rem) == 0 then Ok num else Err (Malformatted "remaining characters after number") Err e -> Err e # Decodes a number and returns the number and any unused input bDecodeNumAndRem: Bytes -> Result {num: I64, rem: (Bytes)} [Malformatted Str] bDecodeNumAndRem = \i -> when (List.first i) is Ok f -> if f == 'i' then when List.findFirstIndex i (\j -> j == 'e') is Ok ix -> {before, others} = List.split i (ix+1) numl = List.sublist before {start: 1, len: ix-1} when (Str.fromUtf8 numl) is Ok nums -> when (Str.toI64 nums) is Ok num -> Ok {num: num, rem: others} Err _e -> Err (Malformatted "failed to parse number from \(nums)") Err _e -> Err (Malformatted "failed to parse utf8") Err _e -> Err (Malformatted "failed to find number end char 'e'") else Err (Malformatted "number didn't start with 'i'") Err _e -> Err (Malformatted "number string was empty") expect res = bDecodeNum (Str.toUtf8 "i32e") res == Ok 32i64 expect errCases = [ {case: "", msg: "number string was empty"}, {case: "foo", msg: "number didn't start with 'i'"}, {case: "i32", msg: "failed to find number end char 'e'"}, {case: "32e", msg: "number didn't start with 'i'"}, {case: "ifooe", msg: "failed to parse number from foo"}, {case: "ie", msg: "failed to parse number from "}, {case: "i", msg: "failed to find number end char 'e'"}, {case: "e", msg: "number didn't start with 'i'"}, {case: "i522222222222222222343423432434322e", msg: "failed to parse number from 522222222222222222343423432434322"}, ] List.all errCases \{case, msg} -> res = bDecodeNum (Str.toUtf8 case) res == Err (Malformatted msg) # Tags for storing heterogenous collections BVal: [ BStr (Bytes), BNum I64, BList (List BVal), BDict (Dict (Bytes) BVal) ] # Detects and decodes any kind of element. bDecodeValueAndRem: (Bytes) -> Result {res: BVal, others: (Bytes)} [Malformatted, End (Bytes)] bDecodeValueAndRem = \i -> when (List.first i) is Ok first -> when first is '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9' -> when bDecodeStrAndRem i is Ok {res, others} -> Ok {res: BStr res, others: others} Err _err -> Err Malformatted 'i' -> when bDecodeNumAndRem i is Ok {num, rem} -> Ok {res: BNum num, others: rem} Err _err -> Err Malformatted 'l' -> when bDecodeListAndRem i is Ok {res, others} -> Ok {res: BList res, others: others} Err _err -> Err Malformatted 'd' -> when bDecodeDictAndRem i is Ok {res, others} -> Ok {res: BDict res, others: others} Err _err -> Err Malformatted 'e' -> r = List.split i 1 Err (End r.others) _ -> Err Malformatted Err _err -> Err Malformatted expect res = bDecodeValueAndRem (Str.toUtf8 "i64efoo") res == Ok {res: BNum 64, others: (Str.toUtf8 "foo")} expect res = bDecodeValueAndRem (Str.toUtf8 "3:foo3:foo") res == Ok {res: BStr (Str.toUtf8 "foo"), others: (Str.toUtf8 "3:foo")} expect res = bDecodeValueAndRem (Str.toUtf8 "efoo") res == Err (End (Str.toUtf8 "foo")) expect res = bDecodeValueAndRem (Str.toUtf8 "z") res == Err Malformatted # Decodes a list. List elements can be any bDecodeList: (Bytes) -> Result (List BVal) [Malformatted] bDecodeList = \i -> when bDecodeListAndRem i is Ok {res, others} -> if (List.len others) == 0 then Ok res else Err Malformatted Err e -> Err e bDecodeListAndRem: (Bytes) -> Result {res: (List BVal), others: (Bytes)} [Malformatted] bDecodeListAndRem = \i -> when List.first i is Ok f -> if f == 'l' then r = List.split i 1 bDecodeListAndRemInternal [] r.others else Err Malformatted Err _e -> Err Malformatted bDecodeListAndRemInternal: List BVal, Bytes -> Result {res: (List BVal), others: (Bytes)} [Malformatted] bDecodeListAndRemInternal = \l, i -> when bDecodeValueAndRem i is Ok {res, others} -> l2 = List.append l res bDecodeListAndRemInternal l2 others Err e -> when e is End others -> Ok {res: l, others: others} Malformatted -> Err Malformatted expect res = bDecodeList (Str.toUtf8 "le") res == Ok [] expect res = bDecodeList (Str.toUtf8 "li123e3:fooe") res == Ok [BNum 123i64, BStr (Str.toUtf8 "foo")] expect res = bDecodeList (Str.toUtf8 "li123e3:fooe123") res == Err Malformatted expect res = bDecodeList (Str.toUtf8 "lli123eee") res == Ok [BList [BNum 123i64]] bDecodeDict: (Bytes) -> Result (Dict (Bytes) BVal) [Malformatted Str] bDecodeDict = \i -> when bDecodeDictAndRem i is Ok {res, others} -> if (List.len others) == 0 then Ok res else Err (Malformatted "bDecodeDict: trailing characters") Err e -> Err e bDecodeDictAndRem: (Bytes) -> Result {res: (Dict (Bytes) BVal), others: (Bytes)} [Malformatted Str] bDecodeDictAndRem = \i -> when List.first i is Ok f -> if f == 'd' then r = List.split i 1 bDecodeDictAndRemInternal (Dict.empty{}) r.others else Err (Malformatted "first character not 'd'") Err _e -> Err (Malformatted "empty input") bDecodeDictAndRemInternal: (Dict (Bytes) BVal), (Bytes) -> Result {res: (Dict (Bytes) BVal), others: (Bytes)} [Malformatted Str] bDecodeDictAndRemInternal = \d, i -> when bDecodeValueAndRem i is Ok r1 -> when r1.res is BStr s -> key = s when bDecodeValueAndRem r1.others is Ok r2 -> val = r2.res d2 = Dict.insert d key val bDecodeDictAndRemInternal d2 r2.others Err e -> when e is End _ -> Err (Malformatted "key with no value") Malformatted -> Err (Malformatted "bDecodeDictAndRemInternal: error decoding value") _ -> Err (Malformatted "bDecodeDictAndRemInternal: wrong key type") Err Malformatted -> Err (Malformatted "bDecodeDictAndRemInternal: error decoding key") Err (End others) -> Ok {res: d, others: others} expect expected = Dict.fromList [ ((Str.toUtf8 "foo"), (BStr (Str.toUtf8 "bar"))), ] res = bDecodeDict (Str.toUtf8 "d3:foo3:bare") res == Ok expected expect nest = Dict.fromList [ ((Str.toUtf8 "foo"), (BStr (Str.toUtf8 "bar"))), ] expected = Dict.fromList [ ((Str.toUtf8 "top"), (BDict nest)), ] res = bDecodeDict (Str.toUtf8 "d3:topd3:foo3:baree") res == Ok expected main = Stdout.line "Hello, World"