Skip to content

Commit

Permalink
feat: bencoder decoder implementation and testing and documenting
Browse files Browse the repository at this point in the history
Signed-off-by: nabil salah <[email protected]>
  • Loading branch information
Nabil-Salah committed Aug 14, 2024
1 parent b0f7aeb commit a8f25b7
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 1 deletion.
32 changes: 32 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "development","main" ]
pull_request:
branches: [ "main" ]

jobs:

Check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22.4'

- name: golangci-lint
uses: golangci/golangci-lint-action@v3

- name: gofmt
run: make format


- name: Test
run: make test
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test:
go test -v -cover ./...

format:
go fmt ./...
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# bencode-nabil
# bencode-nabil

Encoder and Decode of Bencode

## Installation

As a library

```shell
go get github.com/codescalersinternships/bencode-nabil/pkg
```

## Usage

in your Go app you can do something like

```go
package main

import (
"fmt"

bencoder "github.com/codescalersinternships/bencode-nabil/pkg"
)




func main() {
s := "d3:bar4:spam3:fooi42ee"

ret,_ := bencoder.Encoder(s)
fmt.Println(ret)
}

```

## Testing

```shell
make test
```
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/codescalersinternships/bencode-nabil

go 1.22.4

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
176 changes: 176 additions & 0 deletions pkg/bencoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package bencoder

import (
"fmt"
"strconv"
"strings"
)

func readInteger(idx *int, str *string, terminator string) (int64, error) {
out := ""
lim := strings.Index((*str)[*idx:], terminator)
if lim == -1 {
return -1, fmt.Errorf("%s","error of terminator wasn't found")
}
lim = lim + (*idx)
out = (*str)[*idx:lim]
*idx = lim
integer, err := strconv.ParseInt(out, 10, 64)
if err != nil {
return -1, err
}
return integer, nil
}

func readBulkString(idx *int, str *string) (interface{}, error) {
out := ""
length, err := readInteger(idx, str,":")
if err != nil {
return "", err
}
if length == -1 {
return nil, nil
}
(*idx)++
lim := (*idx) + int(length)
if lim > len(*str) {
return "", fmt.Errorf("%s","error of string lenght isn't enough")
}
out = (*str)[*idx:lim]
if out == "-1" {
return "", nil
}
*idx = lim -1
return out, nil
}


func readMap(idx *int, str *string) (map[interface{}]interface{}, error) {
var out = make(map[interface{}]interface{})
var prev interface{}
for i := 0; ; i++ {
var val interface{}
if (*str)[*idx] == 'i' {
(*idx)++
integer, err := readInteger(idx, str,"e")
if err != nil {
return nil, err
}
val = integer
(*idx)++
if i%2 == 1 {
out[prev] = val
}

prev = val
continue
}
if (*str)[*idx] >= '0' && (*str)[*idx] <= '9' {
bulkString, err := readBulkString(idx, str)
if err != nil {
return nil, err
}
val = bulkString
(*idx)++
if i%2 == 1 {
out[prev] = val
}

prev = val
continue
}
if (*str)[*idx] == 'l' {
(*idx)++
array, err := Decoder(*str, idx)
if err != nil {
return nil, err
}
val = array
(*idx)++
if i%2 == 1 {
out[prev] = val
}

prev = val
continue
}

if (*str)[*idx] == 'd' {
(*idx)++
respMap, err := readMap(idx, str)
if err != nil {
return nil, err
}
val = respMap
(*idx)++
if i%2 == 1 {
out[prev] = val
}

prev = val
continue
}
if (*str)[*idx] == 'e' {
break
}
}
return out, nil
}

// Decoder reads an bencoded string, returning an array of interfaces of items
func Decoder(str string, start ...*int) (interface{}, error) {
var out []interface{}
var idx int = 0
if len(start) > 0 {
idx = *start[0]
}
for ; idx < len(str); idx++ {
if str[idx] == 'i' {
idx++
integer, err := readInteger(&idx, &str,"e")
if err != nil {
return nil, err
}
out = append(out, integer)
continue
}
if str[idx] >= '0' && str[idx] <= '9' {
bulkString, err := readBulkString(&idx, &str)
if err != nil {
return nil, err
}
out = append(out, bulkString)
continue
}
if str[idx] == 'l' {
idx++
array, err := Decoder(str, &idx)
if err != nil {
return nil, err
}
out = append(out, array)
continue
}

if str[idx] == 'd' {
idx++
respMap, err := readMap(&idx, &str)
if err != nil {
return nil, err
}
out = append(out, respMap)
continue
}
if str[idx] == 'e' {
break
}
}
if len(start) > 0 {
*start[0] = idx
}
if len(out) == 1 {
return out[0], nil
}
return out, nil
}

94 changes: 94 additions & 0 deletions pkg/bencoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package bencoder

import (
"testing"

"github.com/stretchr/testify/assert"
)


func TestDecoder(t *testing.T) {
tests := []struct {
name string
input string
expected interface{}
expectedError bool
}{
{
name: "Simple string",
input: "4:spam",
expected: "spam",
expectedError: false,
},
{
name: "Integer",
input: "i123e",
expected: int64(123),
expectedError: false,
},
{
name: "List of strings",
input: "l4:spam4:eggse",
expected: []interface{}{"spam", "eggs"},
expectedError: false,
},
{
name: "Dictionary with strings",
input: "d3:cow3:moo4:spam4:eggse",
expected: map[interface {}]interface {}(
map[interface {}]interface {}{
"cow":"moo", "spam":"eggs",
},
),
expectedError: false,
},
{
name: "Invalid bencoded string",
input: "invalid",
expected: nil,
expectedError: true,
},
{
name: "Dictionary with strings and integers",
input: "d3:cow3:moo4:spami123ee",
expected: map[interface {}]interface {}(
map[interface {}]interface {}{
"cow":"moo", "spam":int64(123),
},
),
expectedError: false,
},
{
name: "Dictionary with strings and arrays",
input: "d3:cow3:moo4:spaml4:spam4:eggsee",
expected: map[interface {}]interface {}(
map[interface {}]interface {}{
"cow":"moo",
"spam":[]interface {}{"spam", "eggs"},
},
),
expectedError: false,
},
{
name: "Dictionary with strings and dictionaries",
input: "d3:cow3:moo4:spamd3:cow3:moo4:spam4:eggsee",
expected: map[interface {}]interface {}(
map[interface {}]interface {}{
"cow":"moo",
"spam":map[interface {}]interface {}{"cow":"moo", "spam":"eggs"},
},
),
expectedError: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := Decoder(test.input)
if test.expectedError {
assert.Error(t, err)
}
assert.Equal(t, test.expected, got)
})
}
}

0 comments on commit a8f25b7

Please sign in to comment.