Skip to content
This repository was archived by the owner on Dec 13, 2024. It is now read-only.

Commit 6f7d406

Browse files
committed
init
1 parent b6e16fa commit 6f7d406

File tree

7 files changed

+192
-1
lines changed

7 files changed

+192
-1
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @nikolaydubina

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "gomod"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"

.github/workflows/test.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
permissions: read-all
10+
11+
jobs:
12+
build:
13+
name: Test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: get code
17+
uses: actions/checkout@v4
18+
19+
- name: setup go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: ^1.22
23+
24+
- name: test
25+
run: |
26+
go install github.com/jstemmer/go-junit-report/v2@latest
27+
go test -coverprofile=coverage.out -covermode=atomic -cover -json -v ./... 2>&1 | go-junit-report -set-exit-code > tests.xml
28+
29+
- name: fuzz
30+
run: go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 30s .
31+
32+
- name: Upload test results to Codecov
33+
uses: codecov/test-results-action@v1
34+
with:
35+
token: ${{ secrets.CODECOV_TOKEN }}
36+
files: tests.xml
37+
38+
- name: Upload coverage to Codecov
39+
uses: codecov/codecov-action@v4.1.1
40+
with:
41+
token: ${{ secrets.CODECOV_TOKEN }}
42+
files: coverage.out

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
# base64-reader
1+
Read `base64` as if it was decoded.
2+
3+
Likely you would need `base64.NewDecoder` instead.
4+
5+
* zero memory copy
6+
* fuzz tests
7+
8+
```bash
9+
$ go test -bench=. -benchmem .
10+
goos: darwin
11+
goarch: arm64
12+
pkg: github.com/ndx-technologies/base64io
13+
cpu: Apple M3 Max
14+
BenchmarkBase64Reader/513B-16 1234977 963.0 ns/op 1552 B/op 6 allocs/op
15+
BenchmarkBase64Reader/513B_native-16 1378896 872.4 ns/op 3504 B/op 5 allocs/op
16+
PASS
17+
ok github.com/ndx-technologies/base64io 4.123s
18+
```

base64_reader.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package base64io
2+
3+
import (
4+
"encoding/base64"
5+
"io"
6+
)
7+
8+
// Base64Reader decodes base64 on fly.
9+
type Base64Reader struct {
10+
r io.Reader
11+
d *base64.Encoding
12+
b []byte // read buffer
13+
o []byte // output buffer
14+
s, e int // output buffer start and end
15+
}
16+
17+
func NewBase64Reader(r io.Reader, d *base64.Encoding) *Base64Reader {
18+
return &Base64Reader{r: r, d: d, b: make([]byte, 8), o: make([]byte, 8)}
19+
}
20+
21+
func (s *Base64Reader) Read(b []byte) (int, error) {
22+
if len(b) == 0 {
23+
return 0, nil
24+
}
25+
26+
if s.s < s.e {
27+
if n := copy(b, s.o[s.s:s.e]); n > 0 {
28+
s.s += n
29+
return n, nil
30+
}
31+
}
32+
33+
n, err := s.r.Read(s.b)
34+
if err != nil {
35+
return 0, err
36+
}
37+
38+
m, err := s.d.Decode(s.o, s.b[:n])
39+
40+
l := copy(b, s.o[:m])
41+
42+
if l < len(s.o) {
43+
s.s, s.e = l, m
44+
}
45+
46+
return l, err
47+
}

base64_reader_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package base64io_test
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"testing"
9+
10+
"github.com/ndx-technologies/base64io"
11+
)
12+
13+
func ExampleBase64Reader() {
14+
b := base64.StdEncoding.EncodeToString([]byte("Hello 🌏"))
15+
r := base64io.NewBase64Reader(strings.NewReader(b), base64.StdEncoding)
16+
o, _ := io.ReadAll(r)
17+
fmt.Println(string(b) + " -> " + string(o))
18+
// Output: SGVsbG8g8J+Mjw== -> Hello 🌏
19+
}
20+
21+
func FuzzBase64Reader(f *testing.F) {
22+
f.Add([]byte(nil))
23+
f.Add([]byte{})
24+
f.Add([]byte(" "))
25+
f.Add([]byte(" asdf "))
26+
f.Add([]byte(strings.Repeat("a", 513)))
27+
28+
f.Fuzz(func(t *testing.T, b []byte) {
29+
r := base64io.NewBase64Reader(strings.NewReader(base64.StdEncoding.EncodeToString(b)), base64.StdEncoding)
30+
o, err := io.ReadAll(r)
31+
if err != nil {
32+
t.Error(err)
33+
}
34+
if string(b) != string(o) {
35+
t.Errorf("expected %s, got %s", string(b), string(o))
36+
}
37+
})
38+
}
39+
40+
func BenchmarkBase64Reader(b *testing.B) {
41+
b.Run("513B", func(b *testing.B) {
42+
source := strings.Repeat("a", 513)
43+
s := base64.StdEncoding.EncodeToString([]byte(source))
44+
45+
b.ResetTimer()
46+
for range b.N {
47+
r := base64io.NewBase64Reader(strings.NewReader(s), base64.StdEncoding)
48+
o, err := io.ReadAll(r)
49+
if err != nil {
50+
b.Error(err)
51+
}
52+
if source != string(o) {
53+
b.Errorf("expected %s, got %s", source, string(o))
54+
}
55+
}
56+
})
57+
58+
b.Run("513B_native", func(b *testing.B) {
59+
source := strings.Repeat("a", 513)
60+
s := base64.StdEncoding.EncodeToString([]byte(source))
61+
62+
b.ResetTimer()
63+
for range b.N {
64+
r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
65+
66+
o, err := io.ReadAll(r)
67+
if err != nil {
68+
b.Error(err)
69+
}
70+
if source != string(o) {
71+
b.Errorf("expected %s, got %s", source, string(o))
72+
}
73+
}
74+
})
75+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/ndx-technologies/base64io
2+
3+
go 1.23.0

0 commit comments

Comments
 (0)