Skip to content

Commit dc8e2a6

Browse files
hopehookgopherbot
authored andcommitted
io: add OffsetWriter, NewOffsetWriter
Offsetwriter refers to the design of SectionReader and removes the section parameter n. Since the size of the written data is determined by the user, we cannot know where the end offset of the original data is. The offset of SeekEnd is not valid in Seek method. Fixes #45899. Change-Id: I9d9445aecfa0dd4fc5168f2f65e1e3055c201b45 Reviewed-on: https://go-review.googlesource.com/c/go/+/406776 Run-TryBot: Ian Lance Taylor <[email protected]> Reviewed-by: Joedian Reid <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Ian Lance Taylor <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]>
1 parent 7dad1d2 commit dc8e2a6

File tree

4 files changed

+224
-0
lines changed

4 files changed

+224
-0
lines changed

api/next/45899.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pkg io, type OffsetWriter struct #45899
2+
pkg io, func NewOffsetWriter(WriterAt, int64) *OffsetWriter #45899
3+
pkg io, method (*OffsetWriter) Write([]uint8) (int, error) #45899
4+
pkg io, method (*OffsetWriter) WriteAt([]uint8, int64) (int, error) #45899
5+
pkg io, method (*OffsetWriter) Seek(int64, int) (int64, error) #45899

src/io/export_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ package io
66

77
// exported for test
88
var ErrInvalidWrite = errInvalidWrite
9+
var ErrWhence = errWhence
10+
var ErrOffset = errOffset

src/io/io.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,46 @@ func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
555555
// Size returns the size of the section in bytes.
556556
func (s *SectionReader) Size() int64 { return s.limit - s.base }
557557

558+
// An OffsetWriter maps writes at offset base to offset base+off in the underlying writer.
559+
type OffsetWriter struct {
560+
w WriterAt
561+
base int64 // the original offset
562+
off int64 // the current offset
563+
}
564+
565+
// NewOffsetWriter returns an OffsetWriter that writes to w
566+
// starting at offset off.
567+
func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter {
568+
return &OffsetWriter{w, off, off}
569+
}
570+
571+
func (o *OffsetWriter) Write(p []byte) (n int, err error) {
572+
n, err = o.w.WriteAt(p, o.off)
573+
o.off += int64(n)
574+
return
575+
}
576+
577+
func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error) {
578+
off += o.base
579+
return o.w.WriteAt(p, off)
580+
}
581+
582+
func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error) {
583+
switch whence {
584+
default:
585+
return 0, errWhence
586+
case SeekStart:
587+
offset += o.base
588+
case SeekCurrent:
589+
offset += o.off
590+
}
591+
if offset < o.base {
592+
return 0, errOffset
593+
}
594+
o.off = offset
595+
return offset - o.base, nil
596+
}
597+
558598
// TeeReader returns a Reader that writes to w what it reads from r.
559599
// All reads from r performed through it are matched with
560600
// corresponding writes to w. There is no internal buffering -

src/io/io_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99
"errors"
1010
"fmt"
1111
. "io"
12+
"os"
1213
"strings"
14+
"sync"
15+
"sync/atomic"
1316
"testing"
1417
)
1518

@@ -492,3 +495,177 @@ func TestNopCloserWriterToForwarding(t *testing.T) {
492495
}
493496
}
494497
}
498+
499+
func TestOffsetWriter_Seek(t *testing.T) {
500+
tmpfilename := "TestOffsetWriter_Seek"
501+
tmpfile, err := os.CreateTemp(t.TempDir(), tmpfilename)
502+
if err != nil || tmpfile == nil {
503+
t.Fatalf("CreateTemp(%s) failed: %v", tmpfilename, err)
504+
}
505+
defer tmpfile.Close()
506+
w := NewOffsetWriter(tmpfile, 0)
507+
508+
// Should throw error errWhence if whence is not valid
509+
t.Run("errWhence", func(t *testing.T) {
510+
for _, whence := range []int{-3, -2, -1, 3, 4, 5} {
511+
var offset int64 = 0
512+
gotOff, gotErr := w.Seek(offset, whence)
513+
if gotOff != 0 || gotErr != ErrWhence {
514+
t.Errorf("For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, %v)",
515+
whence, offset, gotOff, gotErr, 0, ErrWhence)
516+
}
517+
}
518+
})
519+
520+
// Should throw error errOffset if offset is negative
521+
t.Run("errOffset", func(t *testing.T) {
522+
for _, whence := range []int{SeekStart, SeekCurrent} {
523+
for offset := int64(-3); offset < 0; offset++ {
524+
gotOff, gotErr := w.Seek(offset, whence)
525+
if gotOff != 0 || gotErr != ErrOffset {
526+
t.Errorf("For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, %v)",
527+
whence, offset, gotOff, gotErr, 0, ErrOffset)
528+
}
529+
}
530+
}
531+
})
532+
533+
// Normal tests
534+
t.Run("normal", func(t *testing.T) {
535+
tests := []struct {
536+
offset int64
537+
whence int
538+
returnOff int64
539+
}{
540+
// keep in order
541+
{whence: SeekStart, offset: 1, returnOff: 1},
542+
{whence: SeekStart, offset: 2, returnOff: 2},
543+
{whence: SeekStart, offset: 3, returnOff: 3},
544+
{whence: SeekCurrent, offset: 1, returnOff: 4},
545+
{whence: SeekCurrent, offset: 2, returnOff: 6},
546+
{whence: SeekCurrent, offset: 3, returnOff: 9},
547+
}
548+
for idx, tt := range tests {
549+
gotOff, gotErr := w.Seek(tt.offset, tt.whence)
550+
if gotOff != tt.returnOff || gotErr != nil {
551+
t.Errorf("%d:: For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, <nil>)",
552+
idx+1, tt.whence, tt.offset, gotOff, gotErr, tt.returnOff)
553+
}
554+
}
555+
})
556+
}
557+
558+
func TestOffsetWriter_WriteAt(t *testing.T) {
559+
const content = "0123456789ABCDEF"
560+
contentSize := int64(len(content))
561+
tmpdir, err := os.MkdirTemp(t.TempDir(), "TestOffsetWriter_WriteAt")
562+
if err != nil {
563+
t.Fatal(err)
564+
}
565+
566+
work := func(off, at int64) {
567+
position := fmt.Sprintf("off_%d_at_%d", off, at)
568+
tmpfile, err := os.CreateTemp(tmpdir, position)
569+
if err != nil || tmpfile == nil {
570+
t.Fatalf("CreateTemp(%s) failed: %v", position, err)
571+
}
572+
defer tmpfile.Close()
573+
574+
var writeN int64
575+
var wg sync.WaitGroup
576+
// Concurrent writes, one byte at a time
577+
for step, value := range []byte(content) {
578+
wg.Add(1)
579+
go func(wg *sync.WaitGroup, tmpfile *os.File, value byte, off, at int64, step int) {
580+
defer wg.Done()
581+
582+
w := NewOffsetWriter(tmpfile, off)
583+
n, e := w.WriteAt([]byte{value}, at+int64(step))
584+
if e != nil {
585+
t.Errorf("WriteAt failed. off: %d, at: %d, step: %d\n error: %v", off, at, step, e)
586+
}
587+
atomic.AddInt64(&writeN, int64(n))
588+
}(&wg, tmpfile, value, off, at, step)
589+
}
590+
wg.Wait()
591+
592+
// Read one more byte to reach EOF
593+
buf := make([]byte, contentSize+1)
594+
readN, err := tmpfile.ReadAt(buf, off+at)
595+
if err != EOF {
596+
t.Fatalf("ReadAt failed: %v", err)
597+
}
598+
readContent := string(buf[:contentSize])
599+
if writeN != int64(readN) || writeN != contentSize || readContent != content {
600+
t.Fatalf("%s:: WriteAt(%s, %d) error. \ngot n: %v, content: %s \nexpected n: %v, content: %v",
601+
position, content, at, readN, readContent, contentSize, content)
602+
}
603+
}
604+
for off := int64(0); off < 2; off++ {
605+
for at := int64(0); at < 2; at++ {
606+
work(off, at)
607+
}
608+
}
609+
}
610+
611+
func TestOffsetWriter_Write(t *testing.T) {
612+
const content = "0123456789ABCDEF"
613+
contentSize := len(content)
614+
tmpdir := t.TempDir()
615+
616+
makeOffsetWriter := func(name string) (*OffsetWriter, *os.File) {
617+
tmpfilename := "TestOffsetWriter_Write_" + name
618+
tmpfile, err := os.CreateTemp(tmpdir, tmpfilename)
619+
if err != nil || tmpfile == nil {
620+
t.Fatalf("CreateTemp(%s) failed: %v", tmpfilename, err)
621+
}
622+
return NewOffsetWriter(tmpfile, 0), tmpfile
623+
}
624+
checkContent := func(name string, f *os.File) {
625+
// Read one more byte to reach EOF
626+
buf := make([]byte, contentSize+1)
627+
readN, err := f.ReadAt(buf, 0)
628+
if err != EOF {
629+
t.Fatalf("ReadAt failed, err: %v", err)
630+
}
631+
readContent := string(buf[:contentSize])
632+
if readN != contentSize || readContent != content {
633+
t.Fatalf("%s error. \ngot n: %v, content: %s \nexpected n: %v, content: %v",
634+
name, readN, readContent, contentSize, content)
635+
}
636+
}
637+
638+
var name string
639+
name = "Write"
640+
t.Run(name, func(t *testing.T) {
641+
// Write directly (off: 0, at: 0)
642+
// Write content to file
643+
w, f := makeOffsetWriter(name)
644+
defer f.Close()
645+
for _, value := range []byte(content) {
646+
n, err := w.Write([]byte{value})
647+
if err != nil {
648+
t.Fatalf("Write failed, n: %d, err: %v", n, err)
649+
}
650+
}
651+
checkContent(name, f)
652+
653+
// Copy -> Write
654+
// Copy file f to file f2
655+
name = "Copy"
656+
w2, f2 := makeOffsetWriter(name)
657+
defer f2.Close()
658+
Copy(w2, f)
659+
checkContent(name, f2)
660+
})
661+
662+
// Copy -> WriteTo -> Write
663+
// Note: strings.Reader implements the io.WriterTo interface.
664+
name = "Write_Of_Copy_WriteTo"
665+
t.Run(name, func(t *testing.T) {
666+
w, f := makeOffsetWriter(name)
667+
defer f.Close()
668+
Copy(w, strings.NewReader(content))
669+
checkContent(name, f)
670+
})
671+
}

0 commit comments

Comments
 (0)