diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..ec22bcc --- /dev/null +++ b/.github/workflows/go.yml @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d5c96bd --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +test: + go test -v -cover ./... + +format: + go fmt ./... \ No newline at end of file diff --git a/README.md b/README.md index b2a1aee..5d87504 100644 --- a/README.md +++ b/README.md @@ -1 +1,42 @@ -# bencode-nabil \ No newline at end of file +# 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 +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..62b38b8 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..97d1e39 --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/bencoder.go b/pkg/bencoder.go new file mode 100644 index 0000000..c7035bb --- /dev/null +++ b/pkg/bencoder.go @@ -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 +} + diff --git a/pkg/bencoder_test.go b/pkg/bencoder_test.go new file mode 100644 index 0000000..cf4142f --- /dev/null +++ b/pkg/bencoder_test.go @@ -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) + }) + } +} \ No newline at end of file