Skip to content

Commit ca09cb6

Browse files
authored
[safeio] Add the ability to perform safe concatenations like cat (#687)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description - add a way to combine streams like `cat` in a safe manner ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update).
1 parent 1207866 commit ca09cb6

File tree

4 files changed

+111
-8
lines changed

4 files changed

+111
-8
lines changed

changes/20250826104813.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[safeio]` Add the ability to perform safe concatenations like `cat`

utils/safeio/copy.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@ import (
88
"github.com/ARM-software/golang-utils/utils/parallelisation"
99
)
1010

11-
// CopyDataWithContext copies from src to dst similarly to io.Copy but with context control to stop when asked to.
11+
// CopyDataWithContext copies from src to dst similarly to io.Copy but with context control to stop when asked.
1212
func CopyDataWithContext(ctx context.Context, src io.Reader, dst io.Writer) (copied int64, err error) {
1313
return copyDataWithContext(ctx, src, dst, io.Copy)
1414
}
1515

16-
// CopyNWithContext copies n bytes from src to dst similarly to io.CopyN but with context control to stop when asked to.
16+
// CopyNWithContext copies n bytes from src to dst similarly to io.CopyN but with context control to stop when asked.
1717
func CopyNWithContext(ctx context.Context, src io.Reader, dst io.Writer, n int64) (copied int64, err error) {
1818
return copyDataWithContext(ctx, src, dst, func(dst io.Writer, src io.Reader) (int64, error) { return io.CopyN(dst, src, n) })
1919
}
2020

21+
// CatN concatenates n bytes from multiple sources to dst. It is intended to provide functionality quite similar to `cat` posix command but with context control.
22+
func CatN(ctx context.Context, dst io.Writer, n int64, src ...io.Reader) (copied int64, err error) {
23+
return CopyNWithContext(ctx, NewContextualMultipleReader(ctx, src...), dst, n)
24+
}
25+
26+
// Cat concatenates bytes from multiple sources to dst. It is intended to provide functionality quite similar to `cat` posix command but with context control.
27+
func Cat(ctx context.Context, dst io.Writer, src ...io.Reader) (copied int64, err error) {
28+
return CopyDataWithContext(ctx, NewContextualMultipleReader(ctx, src...), dst)
29+
}
30+
2131
func copyDataWithContext(ctx context.Context, src io.Reader, dst io.Writer, copyFunc func(io.Writer, io.Reader) (int64, error)) (copied int64, err error) {
2232
err = parallelisation.DetermineContextError(ctx)
2333
if err != nil {

utils/safeio/copy_test.go

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/ARM-software/golang-utils/utils/commonerrors"
1313
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
14+
"github.com/ARM-software/golang-utils/utils/safecast"
1415
)
1516

1617
func TestCopyDataWithContext(t *testing.T) {
@@ -23,7 +24,7 @@ func TestCopyDataWithContext(t *testing.T) {
2324
n2, err := CopyDataWithContext(context.Background(), &buf1, &buf2)
2425
require.NoError(t, err)
2526
require.NotZero(t, n2)
26-
assert.Equal(t, int64(len(text)), n2)
27+
assert.Equal(t, safecast.ToInt64(len(text)), n2)
2728
assert.Equal(t, text, buf2.String())
2829

2930
ctx, cancel := context.WithCancel(context.Background())
@@ -49,10 +50,10 @@ func TestCopyNWithContext(t *testing.T) {
4950
require.NoError(t, err)
5051
require.NotZero(t, n)
5152
assert.Equal(t, len(text), n)
52-
n2, err := CopyNWithContext(context.Background(), &buf1, &buf2, int64(len(text)))
53+
n2, err := CopyNWithContext(context.Background(), &buf1, &buf2, safecast.ToInt64(len(text)))
5354
require.NoError(t, err)
5455
require.NotZero(t, n2)
55-
assert.Equal(t, int64(len(text)), n2)
56+
assert.Equal(t, safecast.ToInt64(len(text)), n2)
5657
assert.Equal(t, text, buf2.String())
5758

5859
ctx, cancel := context.WithCancel(context.Background())
@@ -64,7 +65,7 @@ func TestCopyNWithContext(t *testing.T) {
6465
assert.Equal(t, len(text), n)
6566

6667
cancel()
67-
n2, err = CopyNWithContext(ctx, &buf1, &buf2, int64(len(text)))
68+
n2, err = CopyNWithContext(ctx, &buf1, &buf2, safecast.ToInt64(len(text)))
6869
require.Error(t, err)
6970
errortest.AssertError(t, err, commonerrors.ErrCancelled)
7071
assert.Zero(t, n2)
@@ -74,8 +75,91 @@ func TestCopyNWithContext(t *testing.T) {
7475
require.NoError(t, err)
7576
require.NotZero(t, n)
7677
assert.Equal(t, len(text), n)
77-
n2, err = CopyNWithContext(context.Background(), &buf1, &buf2, int64(len(text)-1))
78+
n2, err = CopyNWithContext(context.Background(), &buf1, &buf2, safecast.ToInt64(len(text)-1))
7879
require.NoError(t, err)
7980
require.NotZero(t, n2)
80-
assert.Equal(t, int64(len(text)-1), n2)
81+
assert.Equal(t, safecast.ToInt64(len(text)-1), n2)
82+
}
83+
84+
func TestCat(t *testing.T) {
85+
var buf1, buf2, buf3 bytes.Buffer
86+
text1 := faker.Sentence()
87+
text2 := faker.Paragraph()
88+
n, err := WriteString(context.Background(), &buf1, text1)
89+
require.NoError(t, err)
90+
require.NotZero(t, n)
91+
assert.Equal(t, len(text1), n)
92+
n, err = WriteString(context.Background(), &buf2, text2)
93+
require.NoError(t, err)
94+
require.NotZero(t, n)
95+
assert.Equal(t, len(text2), n)
96+
n3, err := Cat(context.Background(), &buf3, &buf1, &buf2)
97+
require.NoError(t, err)
98+
require.NotZero(t, n3)
99+
assert.Equal(t, safecast.ToInt64(len(text1)+len(text2)), n3)
100+
assert.Equal(t, text1+text2, buf3.String())
101+
102+
ctx, cancel := context.WithCancel(context.Background())
103+
buf1.Reset()
104+
buf2.Reset()
105+
buf3.Reset()
106+
n, err = WriteString(context.Background(), &buf1, text1)
107+
require.NoError(t, err)
108+
require.NotZero(t, n)
109+
assert.Equal(t, len(text1), n)
110+
n, err = WriteString(context.Background(), &buf2, text2)
111+
require.NoError(t, err)
112+
require.NotZero(t, n)
113+
assert.Equal(t, len(text2), n)
114+
115+
cancel()
116+
n3, err = Cat(ctx, &buf3, &buf1, &buf2)
117+
require.Error(t, err)
118+
errortest.AssertError(t, err, commonerrors.ErrCancelled)
119+
assert.Zero(t, n3)
120+
assert.Empty(t, buf3.String())
121+
}
122+
123+
func TestCatN(t *testing.T) {
124+
var buf1, buf2, buf3 bytes.Buffer
125+
text1 := faker.Sentence()
126+
text2 := faker.Paragraph()
127+
n, err := WriteString(context.Background(), &buf1, text1)
128+
require.NoError(t, err)
129+
require.NotZero(t, n)
130+
assert.Equal(t, len(text1), n)
131+
n, err = WriteString(context.Background(), &buf2, text2)
132+
require.NoError(t, err)
133+
require.NotZero(t, n)
134+
assert.Equal(t, len(text2), n)
135+
n3, err := CatN(context.Background(), &buf3, safecast.ToInt64(len(text1)+len(text2)), &buf1, &buf2)
136+
require.NoError(t, err)
137+
require.NotZero(t, n3)
138+
assert.Equal(t, safecast.ToInt64(len(text1)+len(text2)), n3)
139+
assert.Equal(t, text1+text2, buf3.String())
140+
141+
ctx, cancel := context.WithCancel(context.Background())
142+
buf1.Reset()
143+
buf2.Reset()
144+
buf3.Reset()
145+
n, err = WriteString(context.Background(), &buf1, text1)
146+
require.NoError(t, err)
147+
require.NotZero(t, n)
148+
assert.Equal(t, len(text1), n)
149+
n, err = WriteString(context.Background(), &buf2, text2)
150+
require.NoError(t, err)
151+
require.NotZero(t, n)
152+
assert.Equal(t, len(text2), n)
153+
154+
cancel()
155+
n3, err = CatN(ctx, &buf3, safecast.ToInt64(len(text1)+len(text2)), &buf1, &buf2)
156+
require.Error(t, err)
157+
errortest.AssertError(t, err, commonerrors.ErrCancelled)
158+
assert.Zero(t, n3)
159+
assert.Empty(t, buf3.String())
160+
161+
n3, err = CatN(context.Background(), &buf3, safecast.ToInt64(len(text1)+1), &buf1, &buf2)
162+
require.NoError(t, err)
163+
require.NotZero(t, n3)
164+
assert.Equal(t, safecast.ToInt64(len(text1)+1), n3)
81165
}

utils/safeio/read.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ func NewContextualReader(ctx context.Context, reader io.Reader) io.Reader {
7676
return contextio.NewReader(ctx, reader)
7777
}
7878

79+
func NewContextualMultipleReader(ctx context.Context, reader ...io.Reader) io.Reader {
80+
readers := make([]io.Reader, len(reader))
81+
for i := range reader {
82+
readers[i] = NewContextualReader(ctx, reader[i])
83+
}
84+
return io.MultiReader(readers...)
85+
}
86+
7987
// NewContextualReaderFrom returns a io.ReaderFrom which is context aware.
8088
// Context state is checked BEFORE every Read, Write, Copy.
8189
func NewContextualReaderFrom(ctx context.Context, reader io.ReaderFrom) io.ReaderFrom {

0 commit comments

Comments
 (0)