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