Line data Source code
1 : // Copyright 2011 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2 : // of this source code is governed by a BSD-style license that can be found in
3 : // the LICENSE file.
4 :
5 : package sstable
6 :
7 : import (
8 : "bytes"
9 : "cmp"
10 : "context"
11 : "encoding/binary"
12 : "fmt"
13 : "io"
14 : "slices"
15 : "unsafe"
16 :
17 : "github.com/cockroachdb/pebble/internal/base"
18 : "github.com/cockroachdb/pebble/internal/bytealloc"
19 : "github.com/cockroachdb/pebble/internal/sstableinternal"
20 : "github.com/cockroachdb/pebble/objstorage"
21 : "github.com/cockroachdb/pebble/sstable/block"
22 : "github.com/cockroachdb/pebble/sstable/rowblk"
23 : )
24 :
25 : // Layout describes the block organization of an sstable.
26 : type Layout struct {
27 : // NOTE: changes to fields in this struct should also be reflected in
28 : // ValidateBlockChecksums, which validates a static list of BlockHandles
29 : // referenced in this struct.
30 :
31 : Data []block.HandleWithProperties
32 : Index []block.Handle
33 : TopIndex block.Handle
34 : Filter block.Handle
35 : RangeDel block.Handle
36 : RangeKey block.Handle
37 : ValueBlock []block.Handle
38 : ValueIndex block.Handle
39 : Properties block.Handle
40 : MetaIndex block.Handle
41 : Footer block.Handle
42 : Format TableFormat
43 : }
44 :
45 : // Describe returns a description of the layout. If the verbose parameter is
46 : // true, details of the structure of each block are returned as well.
47 : func (l *Layout) Describe(
48 : w io.Writer, verbose bool, r *Reader, fmtRecord func(key *base.InternalKey, value []byte),
49 1 : ) {
50 1 : ctx := context.TODO()
51 1 : type namedBlockHandle struct {
52 1 : block.Handle
53 1 : name string
54 1 : }
55 1 : var blocks []namedBlockHandle
56 1 :
57 1 : for i := range l.Data {
58 1 : blocks = append(blocks, namedBlockHandle{l.Data[i].Handle, "data"})
59 1 : }
60 1 : for i := range l.Index {
61 1 : blocks = append(blocks, namedBlockHandle{l.Index[i], "index"})
62 1 : }
63 1 : if l.TopIndex.Length != 0 {
64 1 : blocks = append(blocks, namedBlockHandle{l.TopIndex, "top-index"})
65 1 : }
66 1 : if l.Filter.Length != 0 {
67 1 : blocks = append(blocks, namedBlockHandle{l.Filter, "filter"})
68 1 : }
69 1 : if l.RangeDel.Length != 0 {
70 1 : blocks = append(blocks, namedBlockHandle{l.RangeDel, "range-del"})
71 1 : }
72 1 : if l.RangeKey.Length != 0 {
73 1 : blocks = append(blocks, namedBlockHandle{l.RangeKey, "range-key"})
74 1 : }
75 1 : for i := range l.ValueBlock {
76 1 : blocks = append(blocks, namedBlockHandle{l.ValueBlock[i], "value-block"})
77 1 : }
78 1 : if l.ValueIndex.Length != 0 {
79 1 : blocks = append(blocks, namedBlockHandle{l.ValueIndex, "value-index"})
80 1 : }
81 1 : if l.Properties.Length != 0 {
82 1 : blocks = append(blocks, namedBlockHandle{l.Properties, "properties"})
83 1 : }
84 1 : if l.MetaIndex.Length != 0 {
85 1 : blocks = append(blocks, namedBlockHandle{l.MetaIndex, "meta-index"})
86 1 : }
87 1 : if l.Footer.Length != 0 {
88 1 : if l.Footer.Length == levelDBFooterLen {
89 1 : blocks = append(blocks, namedBlockHandle{l.Footer, "leveldb-footer"})
90 1 : } else {
91 1 : blocks = append(blocks, namedBlockHandle{l.Footer, "footer"})
92 1 : }
93 : }
94 :
95 1 : slices.SortFunc(blocks, func(a, b namedBlockHandle) int {
96 1 : return cmp.Compare(a.Offset, b.Offset)
97 1 : })
98 : // TODO(jackson): This function formats offsets within blocks by adding the
99 : // block's offset. A block's offset is an offset in the physical, compressed
100 : // file whereas KV pairs offsets are within the uncompressed block. This is
101 : // confusing and can result in blocks' KVs offsets overlapping one another.
102 : // We should just print offsets relative to the block start.
103 :
104 1 : for i := range blocks {
105 1 : b := &blocks[i]
106 1 : fmt.Fprintf(w, "%10d %s (%d)\n", b.Offset, b.name, b.Length)
107 1 :
108 1 : if !verbose {
109 1 : continue
110 : }
111 1 : if b.name == "filter" {
112 0 : continue
113 : }
114 :
115 1 : if b.name == "footer" || b.name == "leveldb-footer" {
116 1 : trailer, offset := make([]byte, b.Length), b.Offset
117 1 : _ = r.readable.ReadAt(ctx, trailer, int64(offset))
118 1 :
119 1 : if b.name == "footer" {
120 1 : checksumType := block.ChecksumType(trailer[0])
121 1 : fmt.Fprintf(w, "%10d checksum type: %s\n", offset, checksumType)
122 1 : trailer, offset = trailer[1:], offset+1
123 1 : }
124 :
125 1 : metaHandle, n := binary.Uvarint(trailer)
126 1 : metaLen, m := binary.Uvarint(trailer[n:])
127 1 : fmt.Fprintf(w, "%10d meta: offset=%d, length=%d\n", offset, metaHandle, metaLen)
128 1 : trailer, offset = trailer[n+m:], offset+uint64(n+m)
129 1 :
130 1 : indexHandle, n := binary.Uvarint(trailer)
131 1 : indexLen, m := binary.Uvarint(trailer[n:])
132 1 : fmt.Fprintf(w, "%10d index: offset=%d, length=%d\n", offset, indexHandle, indexLen)
133 1 : trailer, offset = trailer[n+m:], offset+uint64(n+m)
134 1 :
135 1 : fmt.Fprintf(w, "%10d [padding]\n", offset)
136 1 :
137 1 : trailing := 12
138 1 : if b.name == "leveldb-footer" {
139 0 : trailing = 8
140 0 : }
141 :
142 1 : offset += uint64(len(trailer) - trailing)
143 1 : trailer = trailer[len(trailer)-trailing:]
144 1 :
145 1 : if b.name == "footer" {
146 1 : version := trailer[:4]
147 1 : fmt.Fprintf(w, "%10d version: %d\n", offset, binary.LittleEndian.Uint32(version))
148 1 : trailer, offset = trailer[4:], offset+4
149 1 : }
150 :
151 1 : magicNumber := trailer
152 1 : fmt.Fprintf(w, "%10d magic number: 0x%x\n", offset, magicNumber)
153 1 :
154 1 : continue
155 : }
156 :
157 1 : h, err := r.readBlock(
158 1 : context.Background(), b.Handle, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* iterStats */, nil /* buffer pool */)
159 1 : if err != nil {
160 0 : fmt.Fprintf(w, " [err: %s]\n", err)
161 0 : continue
162 : }
163 :
164 1 : formatTrailer := func() {
165 1 : trailer := make([]byte, block.TrailerLen)
166 1 : offset := int64(b.Offset + b.Length)
167 1 : _ = r.readable.ReadAt(ctx, trailer, offset)
168 1 : algo := block.CompressionIndicator(trailer[0])
169 1 : checksum := binary.LittleEndian.Uint32(trailer[1:])
170 1 : fmt.Fprintf(w, "%10d [trailer compression=%s checksum=0x%04x]\n", offset, algo, checksum)
171 1 : }
172 :
173 1 : var lastKey InternalKey
174 1 : switch b.name {
175 1 : case "data", "range-del", "range-key":
176 1 : iter, _ := rowblk.NewIter(r.Compare, r.Split, h.Get(), NoTransforms)
177 1 : iter.Describe(w, b.Offset, func(w io.Writer, key *base.InternalKey, value []byte, enc rowblk.KVEncoding) {
178 1 :
179 1 : // The format of the numbers in the record line is:
180 1 : //
181 1 : // (<total> = <length> [<shared>] + <unshared> + <value>)
182 1 : //
183 1 : // <total> is the total number of bytes for the record.
184 1 : // <length> is the size of the 3 varint encoded integers for <shared>,
185 1 : // <unshared>, and <value>.
186 1 : // <shared> is the number of key bytes shared with the previous key.
187 1 : // <unshared> is the number of unshared key bytes.
188 1 : // <value> is the number of value bytes.
189 1 : fmt.Fprintf(w, "%10d record (%d = %d [%d] + %d + %d)",
190 1 : b.Offset+uint64(enc.Offset), enc.Length,
191 1 : enc.Length-int32(enc.KeyUnshared+enc.ValueLen), enc.KeyShared, enc.KeyUnshared, enc.ValueLen)
192 1 : if enc.IsRestart {
193 1 : fmt.Fprintf(w, " [restart]\n")
194 1 : } else {
195 1 : fmt.Fprintf(w, "\n")
196 1 : }
197 1 : if fmtRecord != nil {
198 1 : fmt.Fprintf(w, " ")
199 1 : if l.Format < TableFormatPebblev3 {
200 1 : fmtRecord(key, value)
201 1 : } else {
202 1 : if key.Kind() != InternalKeyKindSet {
203 1 : fmtRecord(key, value)
204 1 : } else if !block.ValuePrefix(value[0]).IsValueHandle() {
205 1 : fmtRecord(key, value[1:])
206 1 : } else {
207 1 : vh := decodeValueHandle(value[1:])
208 1 : fmtRecord(key, []byte(fmt.Sprintf("value handle %+v", vh)))
209 1 : }
210 : }
211 : }
212 :
213 1 : if b.name == "data" {
214 1 : if base.InternalCompare(r.Compare, lastKey, *key) >= 0 {
215 1 : fmt.Fprintf(w, " WARNING: OUT OF ORDER KEYS!\n")
216 1 : }
217 1 : lastKey.Trailer = key.Trailer
218 1 : lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...)
219 : }
220 : })
221 1 : formatTrailer()
222 1 : case "index", "top-index":
223 1 : iter, _ := rowblk.NewIter(r.Compare, r.Split, h.Get(), NoTransforms)
224 1 : iter.Describe(w, b.Offset, func(w io.Writer, key *base.InternalKey, value []byte, enc rowblk.KVEncoding) {
225 1 : bh, err := block.DecodeHandleWithProperties(value)
226 1 : if err != nil {
227 0 : fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(enc.Offset), err)
228 0 : return
229 0 : }
230 1 : fmt.Fprintf(w, "%10d block:%d/%d",
231 1 : b.Offset+uint64(enc.Offset), bh.Offset, bh.Length)
232 1 : if enc.IsRestart {
233 1 : fmt.Fprintf(w, " [restart]\n")
234 1 : } else {
235 0 : fmt.Fprintf(w, "\n")
236 0 : }
237 : })
238 1 : formatTrailer()
239 1 : case "properties":
240 1 : iter, _ := rowblk.NewRawIter(r.Compare, h.Get())
241 1 : iter.Describe(w, b.Offset,
242 1 : func(w io.Writer, key *base.InternalKey, value []byte, enc rowblk.KVEncoding) {
243 1 : fmt.Fprintf(w, "%10d %s (%d)", b.Offset+uint64(enc.Offset), key.UserKey, enc.Length)
244 1 : })
245 1 : formatTrailer()
246 1 : case "meta-index":
247 1 : iter, _ := rowblk.NewRawIter(r.Compare, h.Get())
248 1 : iter.Describe(w, b.Offset,
249 1 : func(w io.Writer, key *base.InternalKey, value []byte, enc rowblk.KVEncoding) {
250 1 : var bh block.Handle
251 1 : var n int
252 1 : var vbih valueBlocksIndexHandle
253 1 : isValueBlocksIndexHandle := false
254 1 : if bytes.Equal(iter.Key().UserKey, []byte(metaValueIndexName)) {
255 1 : vbih, n, err = decodeValueBlocksIndexHandle(value)
256 1 : bh = vbih.h
257 1 : isValueBlocksIndexHandle = true
258 1 : } else {
259 1 : bh, n = block.DecodeHandle(value)
260 1 : }
261 1 : if n == 0 || n != len(value) {
262 0 : fmt.Fprintf(w, "%10d [err: %s]\n", enc.Offset, err)
263 0 : return
264 0 : }
265 1 : var vbihStr string
266 1 : if isValueBlocksIndexHandle {
267 1 : vbihStr = fmt.Sprintf(" value-blocks-index-lengths: %d(num), %d(offset), %d(length)",
268 1 : vbih.blockNumByteLength, vbih.blockOffsetByteLength, vbih.blockLengthByteLength)
269 1 : }
270 1 : fmt.Fprintf(w, "%10d %s block:%d/%d%s",
271 1 : b.Offset+uint64(enc.Offset), iter.Key().UserKey, bh.Offset, bh.Length, vbihStr)
272 : })
273 1 : formatTrailer()
274 1 : case "value-block":
275 : // We don't peer into the value-block since it can't be interpreted
276 : // without the valueHandles.
277 1 : case "value-index":
278 : // We have already read the value-index to construct the list of
279 : // value-blocks, so no need to do it again.
280 : }
281 :
282 1 : h.Release()
283 : }
284 :
285 1 : last := blocks[len(blocks)-1]
286 1 : fmt.Fprintf(w, "%10d EOF\n", last.Offset+last.Length)
287 : }
288 :
289 : // layoutWriter writes the structure of an sstable to durable storage. It
290 : // accepts serialized blocks, writes them to storage and returns a block handle
291 : // describing the offset and length of the block.
292 : type layoutWriter struct {
293 : writable objstorage.Writable
294 :
295 : // cacheOpts are used to remove blocks written to the sstable from the cache,
296 : // providing a defense in depth against bugs which cause cache collisions.
297 : cacheOpts sstableinternal.CacheOptions
298 :
299 : // options copied from WriterOptions
300 : tableFormat TableFormat
301 : compression block.Compression
302 : checksumType block.ChecksumType
303 :
304 : // offset tracks the current write offset within the writable.
305 : offset uint64
306 : // lastIndexBlockHandle holds the handle to the most recently-written index
307 : // block. It's updated by writeIndexBlock. When writing sstables with a
308 : // single-level index, this field will be updated once. When writing
309 : // sstables with a two-level index, the last update will set the two-level
310 : // index.
311 : lastIndexBlockHandle block.Handle
312 : handles []metaIndexHandle
313 : handlesBuf bytealloc.A
314 : tmp [blockHandleLikelyMaxLen]byte
315 : buf blockBuf
316 : }
317 :
318 1 : func makeLayoutWriter(w objstorage.Writable, opts WriterOptions) layoutWriter {
319 1 : return layoutWriter{
320 1 : writable: w,
321 1 : cacheOpts: opts.internal.CacheOpts,
322 1 : tableFormat: opts.TableFormat,
323 1 : compression: opts.Compression,
324 1 : checksumType: opts.Checksum,
325 1 : buf: blockBuf{
326 1 : checksummer: block.Checksummer{Type: opts.Checksum},
327 1 : },
328 1 : }
329 1 : }
330 :
331 : type metaIndexHandle struct {
332 : key string
333 : encodedBlockHandle []byte
334 : }
335 :
336 : // Abort aborts writing the table, aborting the underlying writable too. Abort
337 : // is idempotent.
338 1 : func (w *layoutWriter) Abort() {
339 1 : if w.writable != nil {
340 1 : w.writable.Abort()
341 1 : w.writable = nil
342 1 : }
343 : }
344 :
345 : // WriteDataBlock constructs a trailer for the provided data block and writes
346 : // the block and trailer to the writer. It returns the block's handle.
347 1 : func (w *layoutWriter) WriteDataBlock(b []byte, buf *blockBuf) (block.Handle, error) {
348 1 : return w.writeBlock(b, w.compression, buf)
349 1 : }
350 :
351 : // WritePrecompressedDataBlock writes a pre-compressed data block and its
352 : // pre-computed trailer to the writer, returning it's block handle.
353 1 : func (w *layoutWriter) WritePrecompressedDataBlock(blk block.PhysicalBlock) (block.Handle, error) {
354 1 : return w.writePrecompressedBlock(blk)
355 1 : }
356 :
357 : // WriteIndexBlock constructs a trailer for the provided index (first or
358 : // second-level) and writes the block and trailer to the writer. It remembers
359 : // the last-written index block's handle and adds it to the file's meta index
360 : // when the writer is finished.
361 1 : func (w *layoutWriter) WriteIndexBlock(b []byte) (block.Handle, error) {
362 1 : h, err := w.writeBlock(b, w.compression, &w.buf)
363 1 : if err == nil {
364 1 : w.lastIndexBlockHandle = h
365 1 : }
366 1 : return h, err
367 : }
368 :
369 : // WriteFilterBlock finishes the provided filter, constructs a trailer and
370 : // writes the block and trailer to the writer. It automatically adds the filter
371 : // block to the file's meta index when the writer is finished.
372 1 : func (w *layoutWriter) WriteFilterBlock(f filterWriter) (bh block.Handle, err error) {
373 1 : b, err := f.finish()
374 1 : if err != nil {
375 0 : return block.Handle{}, err
376 0 : }
377 1 : return w.writeNamedBlock(b, f.metaName())
378 : }
379 :
380 : // WritePropertiesBlock constructs a trailer for the provided properties block
381 : // and writes the block and trailer to the writer. It automatically adds the
382 : // properties block to the file's meta index when the writer is finished.
383 1 : func (w *layoutWriter) WritePropertiesBlock(b []byte) (block.Handle, error) {
384 1 : return w.writeNamedBlock(b, metaPropertiesName)
385 1 : }
386 :
387 : // WriteRangeKeyBlock constructs a trailer for the provided range key block and
388 : // writes the block and trailer to the writer. It automatically adds the range
389 : // key block to the file's meta index when the writer is finished.
390 1 : func (w *layoutWriter) WriteRangeKeyBlock(b []byte) (block.Handle, error) {
391 1 : return w.writeNamedBlock(b, metaRangeKeyName)
392 1 : }
393 :
394 : // WriteRangeDeletionBlock constructs a trailer for the provided range deletion
395 : // block and writes the block and trailer to the writer. It automatically adds
396 : // the range deletion block to the file's meta index when the writer is
397 : // finished.
398 1 : func (w *layoutWriter) WriteRangeDeletionBlock(b []byte) (block.Handle, error) {
399 1 : return w.writeNamedBlock(b, metaRangeDelV2Name)
400 1 : }
401 :
402 1 : func (w *layoutWriter) writeNamedBlock(b []byte, name string) (bh block.Handle, err error) {
403 1 : bh, err = w.writeBlock(b, block.NoCompression, &w.buf)
404 1 : if err == nil {
405 1 : w.recordToMetaindex(name, bh)
406 1 : }
407 1 : return bh, err
408 : }
409 :
410 : // WriteValueBlock writes a pre-finished value block (with the trailer) to the
411 : // writer.
412 1 : func (w *layoutWriter) WriteValueBlock(blk block.PhysicalBlock) (block.Handle, error) {
413 1 : return w.writePrecompressedBlock(blk)
414 1 : }
415 :
416 : func (w *layoutWriter) WriteValueIndexBlock(
417 : blk []byte, vbih valueBlocksIndexHandle,
418 1 : ) (block.Handle, error) {
419 1 : // NB: value index blocks are already finished and contain the block
420 1 : // trailer.
421 1 : // TODO(jackson): can this be refactored to make value blocks less
422 1 : // of a snowflake?
423 1 : off := w.offset
424 1 : w.clearFromCache(off)
425 1 : // Write the bytes to the file.
426 1 : if err := w.writable.Write(blk); err != nil {
427 0 : return block.Handle{}, err
428 0 : }
429 1 : l := uint64(len(blk))
430 1 : w.offset += l
431 1 :
432 1 : n := encodeValueBlocksIndexHandle(w.tmp[:], vbih)
433 1 : w.recordToMetaindexRaw(metaValueIndexName, w.tmp[:n])
434 1 :
435 1 : return block.Handle{Offset: off, Length: l}, nil
436 : }
437 :
438 : func (w *layoutWriter) writeBlock(
439 : b []byte, compression block.Compression, buf *blockBuf,
440 1 : ) (block.Handle, error) {
441 1 : return w.writePrecompressedBlock(block.CompressAndChecksum(
442 1 : &buf.compressedBuf, b, compression, &buf.checksummer))
443 1 : }
444 :
445 : // writePrecompressedBlock writes a pre-compressed block and its
446 : // pre-computed trailer to the writer, returning it's block handle.
447 1 : func (w *layoutWriter) writePrecompressedBlock(blk block.PhysicalBlock) (block.Handle, error) {
448 1 : w.clearFromCache(w.offset)
449 1 : // Write the bytes to the file.
450 1 : n, err := blk.WriteTo(w.writable)
451 1 : if err != nil {
452 0 : return block.Handle{}, err
453 0 : }
454 1 : bh := block.Handle{Offset: w.offset, Length: uint64(blk.LengthWithoutTrailer())}
455 1 : w.offset += uint64(n)
456 1 : return bh, nil
457 : }
458 :
459 : // Write implements io.Writer. This is analogous to writePrecompressedBlock for
460 : // blocks that already incorporate the trailer, and don't need the callee to
461 : // return a BlockHandle.
462 0 : func (w *layoutWriter) Write(blockWithTrailer []byte) (n int, err error) {
463 0 : offset := w.offset
464 0 : w.clearFromCache(offset)
465 0 : w.offset += uint64(len(blockWithTrailer))
466 0 : if err := w.writable.Write(blockWithTrailer); err != nil {
467 0 : return 0, err
468 0 : }
469 0 : return len(blockWithTrailer), nil
470 : }
471 :
472 : // clearFromCache removes the block at the provided offset from the cache. This provides defense in
473 : // depth against bugs which cause cache collisions.
474 1 : func (w *layoutWriter) clearFromCache(offset uint64) {
475 1 : if w.cacheOpts.Cache != nil {
476 1 : // TODO(peter): Alternatively, we could add the uncompressed value to the
477 1 : // cache.
478 1 : w.cacheOpts.Cache.Delete(w.cacheOpts.CacheID, w.cacheOpts.FileNum, offset)
479 1 : }
480 : }
481 :
482 1 : func (w *layoutWriter) recordToMetaindex(key string, h block.Handle) {
483 1 : n := h.EncodeVarints(w.tmp[:])
484 1 : w.recordToMetaindexRaw(key, w.tmp[:n])
485 1 : }
486 :
487 1 : func (w *layoutWriter) recordToMetaindexRaw(key string, h []byte) {
488 1 : var encodedHandle []byte
489 1 : w.handlesBuf, encodedHandle = w.handlesBuf.Alloc(len(h))
490 1 : copy(encodedHandle, h)
491 1 : w.handles = append(w.handles, metaIndexHandle{key: key, encodedBlockHandle: encodedHandle})
492 1 : }
493 :
494 1 : func (w *layoutWriter) IsFinished() bool { return w.writable == nil }
495 :
496 : // Finish serializes the sstable, writing out the meta index block and sstable
497 : // footer and closing the file. It returns the total size of the resulting
498 : // ssatable.
499 1 : func (w *layoutWriter) Finish() (size uint64, err error) {
500 1 : // Sort the meta index handles by key and write the meta index block.
501 1 : slices.SortFunc(w.handles, func(a, b metaIndexHandle) int {
502 1 : return cmp.Compare(a.key, b.key)
503 1 : })
504 1 : bw := rowblk.Writer{RestartInterval: 1}
505 1 : for _, h := range w.handles {
506 1 : bw.AddRaw(unsafe.Slice(unsafe.StringData(h.key), len(h.key)), h.encodedBlockHandle)
507 1 : }
508 1 : metaIndexHandle, err := w.writeBlock(bw.Finish(), block.NoCompression, &w.buf)
509 1 : if err != nil {
510 0 : return 0, err
511 0 : }
512 :
513 : // Write the table footer.
514 1 : footer := footer{
515 1 : format: w.tableFormat,
516 1 : checksum: w.checksumType,
517 1 : metaindexBH: metaIndexHandle,
518 1 : indexBH: w.lastIndexBlockHandle,
519 1 : }
520 1 : encodedFooter := footer.encode(w.tmp[:])
521 1 : if err := w.writable.Write(encodedFooter); err != nil {
522 0 : return 0, err
523 0 : }
524 1 : w.offset += uint64(len(encodedFooter))
525 1 :
526 1 : err = w.writable.Finish()
527 1 : w.writable = nil
528 1 : return w.offset, err
529 : }
|