Skip to content

Commit f1ad691

Browse files
committed
fix: clarify progress interruption behavior
1 parent 9e536bd commit f1ad691

8 files changed

Lines changed: 68 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ if err := decoder.Decode(output, buffer); err != nil {
9898
| `CompressBound(size)` | Conservative output-capacity bound for caller buffers. |
9999
| `WithLevel(level)` | Select Skanda compression level `0..10`; out-of-range values are clamped. |
100100
| `WithDecSpeedBias(value)` | Select decode-speed bias `0..1`; out-of-range values are clamped. |
101-
| `WithProgress(fn)` | Observe compression progress and stop early when the callback returns true. |
101+
| `WithProgress(fn)` | Observe compression progress. If the callback returns true before completion, compression stops with `ErrInterrupted`. |
102102

103103
## Format Compatibility
104104

api.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,38 @@ package skanda
33
import "errors"
44

55
var (
6-
ErrCorrupt = errors.New("skanda: corrupt input")
6+
// ErrCorrupt reports malformed input or an output-size mismatch.
7+
ErrCorrupt = errors.New("skanda: corrupt input")
8+
// ErrUnsupportedEntropy reports a recognized stream that uses an unsupported entropy mode.
79
ErrUnsupportedEntropy = errors.New("skanda: unsupported entropy stream")
10+
// ErrInterrupted reports compression stopped because a progress callback returned true.
11+
ErrInterrupted = errors.New("skanda: interrupted")
812
)
913

14+
// ProgressFunc observes compression progress.
15+
//
16+
// processedBytes is the number of source bytes consumed, and compressedBytes
17+
// is the number of bytes appended to the destination. Returning true stops
18+
// compression early and causes Encode or Compress to return ErrInterrupted.
1019
type ProgressFunc func(processedBytes, compressedBytes int) bool
1120

21+
// Options contains compression settings.
1222
type Options struct {
1323
// Level follows Skanda v1.0's public range and is clamped to 0..10.
14-
Level int
24+
Level int
25+
// DecSpeedBias trades compression ratio for decoder speed and is clamped to 0..1.
1526
DecSpeedBias float64
16-
Progress ProgressFunc
27+
// Progress observes compression progress and can interrupt long encodes.
28+
Progress ProgressFunc
1729
}
1830

31+
// Option configures compression.
1932
type Option func(*Options)
2033

34+
// Encoder reuses compression scratch memory across calls.
35+
//
36+
// An Encoder is not safe for concurrent use. Call Close when the encoder is no
37+
// longer needed to release pooled scratch buffers.
2138
type Encoder struct {
2239
state compressState
2340
levelOptions compressorLevelOptions
@@ -27,22 +44,29 @@ type Encoder struct {
2744
splitter *blockSplitter
2845
}
2946

47+
// Decoder reuses decompression scratch memory across calls.
48+
//
49+
// A Decoder is not safe for concurrent use. Call Close when the decoder is no
50+
// longer needed to release pooled scratch buffers.
3051
type Decoder struct {
3152
state decodeState
3253
}
3354

55+
// WithLevel sets the compression level. Values outside 0..10 are clamped.
3456
func WithLevel(level int) Option {
3557
return func(o *Options) {
3658
o.Level = level
3759
}
3860
}
3961

62+
// WithDecSpeedBias sets the decoder-speed bias. Values outside 0..1 are clamped.
4063
func WithDecSpeedBias(decSpeedBias float64) Option {
4164
return func(o *Options) {
4265
o.DecSpeedBias = decSpeedBias
4366
}
4467
}
4568

69+
// WithProgress installs a compression progress callback.
4670
func WithProgress(progress ProgressFunc) Option {
4771
return func(o *Options) {
4872
o.Progress = progress
@@ -76,13 +100,15 @@ func normalizeOptions(options []Option) Options {
76100
return opts
77101
}
78102

103+
// CompressBound returns a conservative upper bound for compressed output size.
79104
func CompressBound(size int) int {
80105
if size < 0 {
81106
return 0
82107
}
83108
return size + size/1024 + 128
84109
}
85110

111+
// IsUnsupported reports whether err indicates an unsupported encoded feature.
86112
func IsUnsupported(err error) bool {
87113
return errors.Is(err, ErrUnsupportedEntropy)
88114
}

decoder.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func (state *decodeState) uint32s(size int) []uint32 {
6262
return state.distanceScratch
6363
}
6464

65+
// Decompress allocates an output buffer of decompressedSize and decodes src into it.
6566
func Decompress(src []byte, decompressedSize int) ([]byte, error) {
6667
if decompressedSize < 0 {
6768
return nil, ErrCorrupt
@@ -73,6 +74,7 @@ func Decompress(src []byte, decompressedSize int) ([]byte, error) {
7374
return dst, nil
7475
}
7576

77+
// Decode decodes src into dst. dst must have the exact decompressed size.
7678
func Decode(dst, src []byte) error {
7779
if len(dst) > maxSharedDecoderOutputSize {
7880
decodeState := decodeState{scratch: acquireByteBuffer(64 << 10)}
@@ -92,6 +94,7 @@ func Decode(dst, src []byte) error {
9294
return err
9395
}
9496

97+
// Decode decodes src into dst using reusable decoder state.
9598
func (decoder *Decoder) Decode(dst, src []byte) error {
9699
if decoder == nil {
97100
return Decode(dst, src)
@@ -102,6 +105,7 @@ func (decoder *Decoder) Decode(dst, src []byte) error {
102105
return decodeWithState(dst, src, &decoder.state)
103106
}
104107

108+
// Close releases scratch memory held by decoder.
105109
func (decoder *Decoder) Close() {
106110
if decoder == nil {
107111
return

encoder.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package skanda
22

3+
// Compress compresses src and returns a newly allocated Skanda stream.
34
func Compress(src []byte, options ...Option) ([]byte, error) {
45
return Encode(nil, src, options...)
56
}
67

8+
// Encode appends the compressed form of src to dst and returns the extended buffer.
79
func Encode(dst, src []byte, options ...Option) ([]byte, error) {
810
if len(src) <= lastBytes+32 || len(src) > maxSharedEncoderSourceSize {
911
return encodeFresh(dst, src, options...)
@@ -64,7 +66,7 @@ func encodeFresh(dst, src []byte, options ...Option) ([]byte, error) {
6466
pos = blockEnd
6567
if opts.Progress != nil {
6668
if opts.Progress(pos, len(dst)-baseLen) {
67-
return dst, nil
69+
return dst, ErrInterrupted
6870
}
6971
}
7072
}
@@ -77,6 +79,7 @@ func encodeFresh(dst, src []byte, options ...Option) ([]byte, error) {
7779
return dst, nil
7880
}
7981

82+
// Encode appends the compressed form of src to dst using reusable encoder state.
8083
func (encoder *Encoder) Encode(dst, src []byte, options ...Option) ([]byte, error) {
8184
if encoder == nil {
8285
return encodeFresh(dst, src, options...)
@@ -118,7 +121,7 @@ func (encoder *Encoder) Encode(dst, src []byte, options ...Option) ([]byte, erro
118121
pos = blockEnd
119122
if opts.Progress != nil {
120123
if opts.Progress(pos, len(dst)-baseLen) {
121-
return dst, nil
124+
return dst, ErrInterrupted
122125
}
123126
}
124127
}
@@ -131,6 +134,7 @@ func (encoder *Encoder) Encode(dst, src []byte, options ...Option) ([]byte, erro
131134
return dst, nil
132135
}
133136

137+
// Close releases scratch memory held by encoder.
134138
func (encoder *Encoder) Close() {
135139
if encoder == nil {
136140
return

entropy_decode.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ func decodeEntropyWithState(src []byte, cpos *int, state *decodeState) ([]byte,
3737
}
3838
return stream, flags, nil
3939
default:
40-
return nil, 0, ErrCorrupt
40+
return nil, 0, ErrUnsupportedEntropy
4141
}
4242
}

memory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "math/bits"
44

55
var windowLogs = [...]int{31, 24, 24, 23, 23, 22, 22, 21, 21, 20, 20}
66

7+
// EstimateMemory estimates the scratch memory needed for a compression run.
78
func EstimateMemory(size int, level int, decSpeedBias float64) int {
89
if size <= lastBytes+1 {
910
return 0

skanda_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ func TestEncodeProgressReportsAppendedBytes(t *testing.T) {
156156
}
157157
}
158158

159+
func TestEncodeProgressCanInterrupt(t *testing.T) {
160+
src := bytes.Repeat([]byte("interruptible progress payload "), 512)
161+
prefix := []byte("existing data")
162+
got, err := Encode(append([]byte(nil), prefix...), src, WithProgress(func(_, _ int) bool {
163+
return true
164+
}))
165+
if !errors.Is(err, ErrInterrupted) {
166+
t.Fatalf("Encode error = %v, want ErrInterrupted", err)
167+
}
168+
if !bytes.Equal(got[:len(prefix)], prefix) {
169+
t.Fatal("Encode did not preserve destination prefix")
170+
}
171+
}
172+
159173
func TestEncoderReuseMatchesEncode(t *testing.T) {
160174
cases := []struct {
161175
src []byte
@@ -280,6 +294,18 @@ func TestDecodeUsesExactDestinationSize(t *testing.T) {
280294
}
281295
}
282296

297+
func TestDecodeEntropyUnsupportedMode(t *testing.T) {
298+
src := writeHeader(nil, 0, 3, 0)
299+
pos := 0
300+
_, _, err := decodeEntropy(src, &pos)
301+
if !errors.Is(err, ErrUnsupportedEntropy) {
302+
t.Fatalf("decodeEntropy error = %v, want ErrUnsupportedEntropy", err)
303+
}
304+
if !IsUnsupported(err) {
305+
t.Fatalf("IsUnsupported(%v) = false, want true", err)
306+
}
307+
}
308+
283309
func TestLengthStreamUsesSingleByteCodes(t *testing.T) {
284310
lengths := []byte{0, 1, 7, 31, 127, 128, 200, 223, 0, 12, 64}
285311
if !lengthStreamUsesSingleByteCodes(lengths) {

0 commit comments

Comments
 (0)