Skip to content

Commit 10e8421

Browse files
add EncodeBase64 / DecodeBase64 filters (#210)
Co-authored-by: John Arundel <john@bitfieldconsulting.com>
1 parent 6c1c252 commit 10e8421

File tree

4 files changed

+149
-1
lines changed

4 files changed

+149
-1
lines changed

.github/workflows/audit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
name: Security audit
22
on:
3+
pull_request:
34
workflow_dispatch:
45
schedule:
56
- cron: '0 0 * * *'

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
3434
| `>` | [`WriteFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WriteFile) |
3535
| `>>` | [`AppendFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.AppendFile) |
3636
| `$*` | [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) |
37+
| `base64` | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
3738
| `basename` | [`Basename`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) |
3839
| `cat` | [`File`](https://pkg.go.dev/github.com/bitfield/script#File) / [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) |
3940
| `curl` | [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) / [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get) / [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
@@ -290,9 +291,11 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
290291
| [`Basename`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) | removes leading path components from each line, leaving only the filename |
291292
| [`Column`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Column) | Nth column of input |
292293
| [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) | contents of multiple files |
294+
| [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) | input decoded from base64 |
293295
| [`Dirname`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Dirname) | removes filename from each line, leaving only leading path components |
294296
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) | response to supplied HTTP request |
295297
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Echo) | all input replaced by given string |
298+
| [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) | input encoded to base64 |
296299
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Exec) | filtered through external command |
297300
| [`ExecForEach`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecForEach) | execute given command template for each line of input |
298301
| [`Filter`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Filter) | user-supplied function filtering a reader to a writer |
@@ -337,6 +340,7 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
337340

338341
| Version | New |
339342
| ----------- | ------- |
343+
| _next_ | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
340344
| v0.22.0 | [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee), [`WithStderr`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithStderr) |
341345
| v0.21.0 | HTTP support: [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do), [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get), [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
342346
| v0.20.0 | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
@@ -347,7 +351,7 @@ See the [contributor's guide](CONTRIBUTING.md) for some helpful tips if you'd li
347351

348352
# Links
349353

350-
- [Scripting with Go](https://bitfieldconsulting.com/golang/scripting)
354+
- [Scripting with Go](https://bitfieldconsulting.com/posts/scripting)
351355
- [Code Club: Script](https://www.youtube.com/watch?v=6S5EqzVwpEg)
352356
- [Bitfield Consulting](https://bitfieldconsulting.com/)
353357
- [Go books by John Arundel](https://bitfieldconsulting.com/books)

script.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"container/ring"
66
"crypto/sha256"
7+
"encoding/base64"
78
"encoding/hex"
89
"encoding/json"
910
"fmt"
@@ -275,6 +276,18 @@ func (p *Pipe) CountLines() (lines int, err error) {
275276
return lines, p.Error()
276277
}
277278

279+
// DecodeBase64 produces the string represented by the base64 encoded input.
280+
func (p *Pipe) DecodeBase64() *Pipe {
281+
return p.Filter(func(r io.Reader, w io.Writer) error {
282+
decoder := base64.NewDecoder(base64.StdEncoding, r)
283+
_, err := io.Copy(w, decoder)
284+
if err != nil {
285+
return err
286+
}
287+
return nil
288+
})
289+
}
290+
278291
// Dirname reads paths from the pipe, one per line, and produces only the
279292
// parent directories of each path. For example, /usr/local/bin/foo would
280293
// become just /usr/local/bin. This is the complementary operation to
@@ -347,6 +360,19 @@ func (p *Pipe) Echo(s string) *Pipe {
347360
return p.WithReader(strings.NewReader(s))
348361
}
349362

363+
// EncodeBase64 produces the base64 encoding of the input.
364+
func (p *Pipe) EncodeBase64() *Pipe {
365+
return p.Filter(func(r io.Reader, w io.Writer) error {
366+
encoder := base64.NewEncoder(base64.StdEncoding, w)
367+
defer encoder.Close()
368+
_, err := io.Copy(encoder, r)
369+
if err != nil {
370+
return err
371+
}
372+
return nil
373+
})
374+
}
375+
350376
// Error returns any error present on the pipe, or nil otherwise.
351377
func (p *Pipe) Error() error {
352378
if p.mu == nil { // uninitialised pipe

script_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,6 +1850,111 @@ func TestReadReturnsErrorGivenReadErrorOnPipe(t *testing.T) {
18501850
}
18511851
}
18521852

1853+
var base64Cases = []struct {
1854+
name string
1855+
decoded string
1856+
encoded string
1857+
}{
1858+
{
1859+
name: "empty string",
1860+
decoded: "",
1861+
encoded: "",
1862+
},
1863+
{
1864+
name: "single line string",
1865+
decoded: "hello world",
1866+
encoded: "aGVsbG8gd29ybGQ=",
1867+
},
1868+
{
1869+
name: "multi line string",
1870+
decoded: "hello\nthere\nworld\n",
1871+
encoded: "aGVsbG8KdGhlcmUKd29ybGQK",
1872+
},
1873+
}
1874+
1875+
func TestEncodeBase64_CorrectlyEncodes(t *testing.T) {
1876+
t.Parallel()
1877+
for _, tc := range base64Cases {
1878+
t.Run(tc.name, func(t *testing.T) {
1879+
got, err := script.Echo(tc.decoded).EncodeBase64().String()
1880+
if err != nil {
1881+
t.Fatal(err)
1882+
}
1883+
if got != tc.encoded {
1884+
t.Logf("input %q incorrectly encoded:", tc.decoded)
1885+
t.Error(cmp.Diff(tc.encoded, got))
1886+
}
1887+
})
1888+
}
1889+
}
1890+
1891+
func TestDecodeBase64_CorrectlyDecodes(t *testing.T) {
1892+
t.Parallel()
1893+
for _, tc := range base64Cases {
1894+
t.Run(tc.name, func(t *testing.T) {
1895+
got, err := script.Echo(tc.encoded).DecodeBase64().String()
1896+
if err != nil {
1897+
t.Fatal(err)
1898+
}
1899+
if got != tc.decoded {
1900+
t.Logf("input %q incorrectly decoded:", tc.encoded)
1901+
t.Error(cmp.Diff(tc.decoded, got))
1902+
}
1903+
})
1904+
}
1905+
}
1906+
1907+
func TestEncodeBase64_FollowedByDecodeRecoversOriginal(t *testing.T) {
1908+
t.Parallel()
1909+
for _, tc := range base64Cases {
1910+
t.Run(tc.name, func(t *testing.T) {
1911+
decoded, err := script.Echo(tc.decoded).EncodeBase64().DecodeBase64().String()
1912+
if err != nil {
1913+
t.Fatal(err)
1914+
}
1915+
if decoded != tc.decoded {
1916+
t.Error("encode-decode round trip failed:", cmp.Diff(tc.decoded, decoded))
1917+
}
1918+
encoded, err := script.Echo(tc.encoded).DecodeBase64().EncodeBase64().String()
1919+
if err != nil {
1920+
t.Fatal(err)
1921+
}
1922+
if encoded != tc.encoded {
1923+
t.Error("decode-encode round trip failed:", cmp.Diff(tc.encoded, encoded))
1924+
}
1925+
})
1926+
}
1927+
}
1928+
1929+
func TestDecodeBase64_CorrectlyDecodesInputToBytes(t *testing.T) {
1930+
t.Parallel()
1931+
input := "CAAAEA=="
1932+
got, err := script.Echo(input).DecodeBase64().Bytes()
1933+
if err != nil {
1934+
t.Fatal(err)
1935+
}
1936+
want := []byte{8, 0, 0, 16}
1937+
if !bytes.Equal(want, got) {
1938+
t.Logf("input %#v incorrectly decoded:", input)
1939+
t.Error(cmp.Diff(want, got))
1940+
}
1941+
}
1942+
1943+
func TestEncodeBase64_CorrectlyEncodesInputBytes(t *testing.T) {
1944+
t.Parallel()
1945+
input := []byte{8, 0, 0, 16}
1946+
reader := bytes.NewReader(input)
1947+
want := "CAAAEA=="
1948+
got, err := script.NewPipe().WithReader(reader).EncodeBase64().String()
1949+
if err != nil {
1950+
t.Fatal(err)
1951+
}
1952+
if got != want {
1953+
t.Logf("input %#v incorrectly encoded:", input)
1954+
t.Error(cmp.Diff(want, got))
1955+
}
1956+
}
1957+
18531958
func ExampleArgs() {
18541959
script.Args().Stdout()
18551960
// prints command-line arguments
@@ -1969,6 +2074,12 @@ func ExamplePipe_CountLines() {
19692074
// 3
19702075
}
19712076

2077+
func ExamplePipe_DecodeBase64() {
2078+
script.Echo("SGVsbG8sIHdvcmxkIQ==").DecodeBase64().Stdout()
2079+
// Output:
2080+
// Hello, world!
2081+
}
2082+
19722083
func ExamplePipe_Do() {
19732084
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19742085
data, err := io.ReadAll(r.Body)
@@ -2004,6 +2115,12 @@ func ExamplePipe_Echo() {
20042115
// Hello, world!
20052116
}
20062117

2118+
func ExamplePipe_EncodeBase64() {
2119+
script.Echo("Hello, world!").EncodeBase64().Stdout()
2120+
// Output:
2121+
// SGVsbG8sIHdvcmxkIQ==
2122+
}
2123+
20072124
func ExamplePipe_ExitStatus() {
20082125
p := script.Exec("echo")
20092126
fmt.Println(p.ExitStatus())

0 commit comments

Comments
 (0)