blob: c7b28756cf45878ab24b1ac11aa64342acf34938 [file] [log] [blame]
Lorenz Brun1e0e3a42023-06-28 16:40:18 +02001package blockdev
2
3import (
4 "errors"
5 "fmt"
6 "io"
7)
8
Lorenz Brun1e0e3a42023-06-28 16:40:18 +02009var ErrNotBlockDevice = errors.New("not a block device")
10
11// BlockDev represents a generic block device made up of equally-sized blocks.
12// All offsets and intervals are expressed in bytes and must be aligned to
13// BlockSize and are recommended to be aligned to OptimalBlockSize if feasible.
14// Unless stated otherwise, intervals are inclusive-exclusive, i.e. the
15// start byte is included but the end byte is not.
16type BlockDev interface {
17 io.ReaderAt
18 io.WriterAt
19 // BlockSize returns the block size of the block device in bytes. This must
20 // be a power of two and is commonly (but not always) either 512 or 4096.
21 BlockSize() int64
22
23 // BlockCount returns the number of blocks on the block device or -1 if it
24 // is an image with an undefined size.
25 BlockCount() int64
26
27 // OptimalBlockSize returns the optimal block size in bytes for aligning
28 // to as well as issuing I/O. IO operations with block sizes below this
29 // one might incur read-write overhead. This is the larger of the physical
30 // block size and a device-reported value if available.
31 OptimalBlockSize() int64
32
33 // Discard discards a continuous set of blocks. Discarding means the
34 // underlying device gets notified that the data in these blocks is no
35 // longer needed. This can improve performance of the device device (as it
36 // no longer needs to preserve the unused data) as well as bulk erase
37 // operations. This command is advisory and not all implementations support
38 // it. The contents of discarded blocks are implementation-defined.
39 Discard(startByte int64, endByte int64) error
40
41 // Zero zeroes a continouous set of blocks. On certain implementations this
42 // can be significantly faster than just calling Write with zeroes.
43 Zero(startByte, endByte int64) error
44}
45
46func NewRWS(b BlockDev) *ReadWriteSeeker {
47 return &ReadWriteSeeker{b: b}
48}
49
50// ReadWriteSeeker provides an adapter implementing ReadWriteSeeker on top of
51// a blockdev.
52type ReadWriteSeeker struct {
53 b BlockDev
54 currPos int64
55}
56
57func (s *ReadWriteSeeker) Read(p []byte) (n int, err error) {
58 n, err = s.b.ReadAt(p, s.currPos)
59 s.currPos += int64(n)
60 return
61}
62
63func (s *ReadWriteSeeker) Write(p []byte) (n int, err error) {
64 n, err = s.b.WriteAt(p, s.currPos)
65 s.currPos += int64(n)
66 return
67}
68
69func (s *ReadWriteSeeker) Seek(offset int64, whence int) (int64, error) {
70 switch whence {
71 case io.SeekCurrent:
72 s.currPos += offset
73 case io.SeekStart:
74 s.currPos = offset
75 case io.SeekEnd:
76 s.currPos = (s.b.BlockCount() * s.b.BlockSize()) - offset
77 }
78 return s.currPos, nil
79}
80
81var ErrOutOfBounds = errors.New("write out of bounds")
82
83// NewSection returns a new Section, implementing BlockDev over that subset
84// of blocks. The interval is inclusive-exclusive.
85func NewSection(b BlockDev, startBlock, endBlock int64) *Section {
86 return &Section{
87 b: b,
88 startBlock: startBlock,
89 endBlock: endBlock,
90 }
91}
92
93// Section implements BlockDev on a slice of another BlockDev given a startBlock
94// and endBlock.
95type Section struct {
96 b BlockDev
97 startBlock, endBlock int64
98}
99
100func (s *Section) ReadAt(p []byte, off int64) (n int, err error) {
101 bOff := off + (s.startBlock * s.b.BlockSize())
102 bytesToEnd := (s.endBlock * s.b.BlockSize()) - bOff
103 if bytesToEnd <= 0 {
104 return 0, io.EOF
105 }
106 if bytesToEnd < int64(len(p)) {
107 return s.b.ReadAt(p[:bytesToEnd], bOff)
108 }
109 return s.b.ReadAt(p, bOff)
110}
111
112func (s *Section) WriteAt(p []byte, off int64) (n int, err error) {
113 bOff := off + (s.startBlock * s.b.BlockSize())
114 bytesToEnd := (s.endBlock * s.b.BlockSize()) - bOff
115 if bytesToEnd <= 0 {
116 return 0, ErrOutOfBounds
117 }
118 if bytesToEnd < int64(len(p)) {
119 n, err := s.b.WriteAt(p[:bytesToEnd], off+(s.startBlock*s.b.BlockSize()))
120 if err != nil {
121 // If an error happened, prioritize that error
122 return n, err
123 }
124 // Otherwise, return ErrOutOfBounds as even short writes must return an
125 // error.
126 return n, ErrOutOfBounds
127 }
128 return s.b.WriteAt(p, off+(s.startBlock*s.b.BlockSize()))
129}
130
131func (s *Section) BlockCount() int64 {
132 return s.endBlock - s.startBlock
133}
134
135func (s *Section) BlockSize() int64 {
136 return s.b.BlockSize()
137}
138
139func (s *Section) inRange(startByte, endByte int64) error {
140 if startByte > endByte {
141 return fmt.Errorf("invalid range: startByte (%d) bigger than endByte (%d)", startByte, endByte)
142 }
143 sectionLen := s.BlockCount() * s.BlockSize()
144 if startByte >= sectionLen {
145 return fmt.Errorf("startByte (%d) out of range (%d)", startByte, sectionLen)
146 }
147 if endByte > sectionLen {
148 return fmt.Errorf("endBlock (%d) out of range (%d)", endByte, sectionLen)
149 }
150 return nil
151}
152
153func (s *Section) Discard(startByte, endByte int64) error {
154 if err := s.inRange(startByte, endByte); err != nil {
155 return err
156 }
157 return s.b.Discard(s.startBlock+startByte, s.startBlock+endByte)
158}
159
160func (s *Section) OptimalBlockSize() int64 {
161 return s.b.OptimalBlockSize()
162}
163
164func (s *Section) Zero(startByte, endByte int64) error {
165 if err := s.inRange(startByte, endByte); err != nil {
166 return err
167 }
168 return s.b.Zero(s.startBlock+startByte, s.startBlock+endByte)
169}
170
171// GenericZero implements software-based zeroing. This can be used to implement
172// Zero when no acceleration is available or desired.
173func GenericZero(b BlockDev, startByte, endByte int64) error {
174 if startByte%b.BlockSize() != 0 {
175 return fmt.Errorf("startByte (%d) needs to be aligned to block size (%d)", startByte, b.BlockSize())
176 }
177 if endByte%b.BlockSize() != 0 {
178 return fmt.Errorf("endByte (%d) needs to be aligned to block size (%d)", endByte, b.BlockSize())
179 }
180 // Choose buffer size close to 16MiB or the range to be zeroed, whatever
181 // is smaller.
182 bufSizeTarget := int64(16 * 1024 * 1024)
183 if endByte-startByte < bufSizeTarget {
184 bufSizeTarget = endByte - startByte
185 }
186 bufSize := (bufSizeTarget / b.BlockSize()) * b.BlockSize()
187 buf := make([]byte, bufSize)
188 for i := startByte; i < endByte; i += bufSize {
189 if endByte-i < bufSize {
190 buf = buf[:endByte-i]
191 }
192 if _, err := b.WriteAt(buf, i); err != nil {
193 return fmt.Errorf("while writing zeroes: %w", err)
194 }
195 }
196 return nil
197}