Skip to content

Commit 3cf1fa5

Browse files
authored
Merge pull request #576 from RoaringBitmap/validate-run-end
Validate: reject runs past the container end, accept non-minimal run containers
2 parents 142f290 + 2957ffe commit 3cf1fa5

5 files changed

Lines changed: 80 additions & 49 deletions

File tree

‎roaring.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2476,6 +2476,8 @@ func (rb *Bitmap) Describe() {
24762476
// Validate checks if the bitmap is internally consistent.
24772477
// You may call it after deserialization to check that the bitmap is valid.
24782478
// This function returns an error if the bitmap is invalid, nil otherwise.
2479+
// It does not check that each container uses the most compact encoding:
2480+
// call RunOptimize for that.
24792481
func (rb *Bitmap) Validate() error {
24802482
return rb.highlowcontainer.validate()
24812483
}

‎roaring_test.go‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3376,28 +3376,34 @@ func TestBitMapValidationFromDeserialization(t *testing.T) {
33763376
err error
33773377
}{
33783378
{
3379-
name: "Corrupts Run Length vs Num Runs",
3379+
name: "Last Run Extends Past Container End",
33803380
loader: func(bm *Bitmap) {
33813381
bm.AddRange(0, 2)
33823382
bm.AddRange(4, 6)
33833383
bm.AddRange(8, 100)
3384+
bm.RunOptimize() // one run container
33843385
},
33853386
corruptor: func(s []byte) {
3386-
// 21 is the length of the run of the last run/range
3387-
// Shortening causes interval sum to be to short
3388-
s[21] = 1
3387+
// 21-22 is the length of the last run, which starts at 8:
3388+
// 65535 makes it end past 65535
3389+
s[21] = 0xFF
3390+
s[22] = 0xFF
33893391
},
3390-
err: ErrRunIntervalSize,
3392+
err: ErrRunIntervalOutOfRange,
33913393
},
33923394
{
3393-
name: "Corrupts Run Length",
3395+
name: "Run Extends Past Container End",
33943396
loader: func(bm *Bitmap) {
33953397
bm.AddRange(100, 110)
3398+
bm.RunOptimize() // one run container
33963399
},
33973400
corruptor: func(s []byte) {
3398-
s[13] = 0
3401+
// 13-14 is the length of the run, which starts at 100:
3402+
// 65535 makes it end past 65535
3403+
s[13] = 0xFF
3404+
s[14] = 0xFF
33993405
},
3400-
err: ErrRunIntervalSize,
3406+
err: ErrRunIntervalOutOfRange,
34013407
},
34023408
{
34033409
name: "Creates Interval Overlap",
@@ -3467,7 +3473,7 @@ func TestBitMapValidationFromDeserialization(t *testing.T) {
34673473
if tt.err == nil {
34683474
assert.NoError(t, validateErr, "expected validation to succeed when tt.err is nil")
34693475
} else {
3470-
assert.Error(t, validateErr, "expected validation to fail when tt.err is not nil")
3476+
assert.ErrorIs(t, validateErr, tt.err)
34713477
}
34723478

34733479
corruptedDeserializedBitMap = NewBitmap()
@@ -4184,3 +4190,24 @@ func TestBitmapOrBulkMergeCopyOnWriteTailOwnership(t *testing.T) {
41844190
t.Fatalf("source became invalid after tail mutations: %v", err)
41854191
}
41864192
}
4193+
4194+
func TestValidateAcceptsRunContainersThatAreNotTheSmallestEncoding(t *testing.T) {
4195+
// AddOffset splits the run: the first container holds a run of two values,
4196+
// which takes more bytes than an array container would
4197+
bm := NewBitmap()
4198+
bm.AddRange(0, 10)
4199+
bm.RunOptimize()
4200+
shifted := AddOffset64(bm, 65534)
4201+
expected := NewBitmap()
4202+
expected.AddRange(65534, 65544)
4203+
assert.True(t, expected.Equals(shifted))
4204+
assert.NoError(t, shifted.Validate())
4205+
4206+
serialized, err := shifted.ToBytes()
4207+
assert.NoError(t, err)
4208+
deserialized := NewBitmap()
4209+
_, err = deserialized.ReadFrom(bytes.NewReader(serialized))
4210+
assert.NoError(t, err)
4211+
assert.NoError(t, deserialized.Validate())
4212+
assert.True(t, expected.Equals(deserialized))
4213+
}

‎runcontainer.go‎

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@ var (
6565
ErrRunNonSorted = errors.New("runs were not sorted")
6666
ErrRunIntervalEqual = errors.New("intervals were equal")
6767
ErrRunIntervalOverlap = errors.New("intervals overlapped or were continguous")
68-
ErrRunIntervalSize = errors.New("too many intervals relative to data")
69-
MaxNumIntervals = 2048
70-
MaxIntervalsSum = 2048
68+
// Deprecated: validate no longer requires a run container to be the smallest
69+
// encoding of its values, so this error is not returned.
70+
ErrRunIntervalSize = errors.New("too many intervals relative to data")
71+
ErrRunIntervalOutOfRange = errors.New("interval extends past the end of the container")
72+
MaxNumIntervals = 2048
73+
MaxIntervalsSum = 2048
7174
)
7275

7376
func newInterval16Range(start, last uint16) interval16 {
@@ -2947,15 +2950,19 @@ func (rc *runContainer16) validate() error {
29472950
return ErrRunIntervalsEmpty
29482951
}
29492952

2950-
intervalsSum := 0
2953+
// check this first: interval16.last() wraps around past the end of the container
2954+
for _, iv := range rc.iv {
2955+
if int(iv.start)+int(iv.length) > int(MaxUint16) {
2956+
return ErrRunIntervalOutOfRange
2957+
}
2958+
}
2959+
29512960
for outeridx := range rc.iv {
29522961
// The length being stored is the actual length - 1.
29532962
// So we need to add 1 to get the actual length.
29542963
// It is not possible to have a run with length 0.
29552964

29562965
outerInterval := rc.iv[outeridx]
2957-
2958-
intervalsSum += outerInterval.runlen()
29592966
for inneridx := outeridx + 1; inneridx < len(rc.iv); inneridx++ {
29602967

29612968
innerInterval := rc.iv[inneridx]
@@ -2977,27 +2984,7 @@ func (rc *runContainer16) validate() error {
29772984
}
29782985

29792986
}
2980-
/*
2981-
if number of distinct values in the container >= 2048 then
2982-
check that the number of runs is no more than 2047
2983-
(otherwise you could use a bitset container)
2984-
else
2985-
check that the number of runs < (number of distinct values) / 2
2986-
(otherwise you could use an array container)
2987-
*/
2988-
2989-
sizeAsRunContainer := runContainer16SerializedSizeInBytes(len(rc.iv))
2990-
sizeAsBitmapContainer := bitmapContainerSizeInBytes()
2991-
sizeAsArrayContainer := arrayContainerSizeInBytes(intervalsSum)
2992-
// this is always ok:
2993-
if sizeAsRunContainer < minOfInt(sizeAsBitmapContainer, sizeAsArrayContainer) {
2994-
return nil
2995-
}
2996-
if sizeAsRunContainer >= sizeAsBitmapContainer {
2997-
return ErrRunIntervalSize
2998-
}
2999-
if sizeAsRunContainer >= sizeAsArrayContainer {
3000-
return ErrRunIntervalSize
3001-
}
2987+
// A run container need not be the smallest encoding of its values: operations such as
2988+
// AddOffset can produce run containers that runOptimize would convert.
30022989
return nil
30032990
}

‎runcontainer_test.go‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2589,8 +2589,9 @@ func TestIntervalValidationFailing(t *testing.T) {
25892589
rc.iv = append(rc.iv, a)
25902590

25912591
}
2592-
assert.ErrorIs(t, rc.validate(), ErrRunIntervalSize)
2593-
// too many small runs, use array
2592+
// more runs than a bitmap container would need is still well formed
2593+
assert.NoError(t, rc.validate())
2594+
// more runs than an array container would need is still well formed
25942595
rc = &runContainer16{}
25952596
start = -3
25962597
for i := 0; i < 10; i++ {
@@ -2600,7 +2601,15 @@ func TestIntervalValidationFailing(t *testing.T) {
26002601
rc.iv = append(rc.iv, a)
26012602

26022603
}
2603-
assert.ErrorIs(t, rc.validate(), ErrRunIntervalSize)
2604+
assert.NoError(t, rc.validate())
2605+
2606+
// a run must end within the container
2607+
rc = &runContainer16{iv: []interval16{{start: 65000, length: 535}}}
2608+
assert.NoError(t, rc.validate())
2609+
rc = &runContainer16{iv: []interval16{{start: 65000, length: 536}}}
2610+
assert.ErrorIs(t, rc.validate(), ErrRunIntervalOutOfRange)
2611+
rc = &runContainer16{iv: []interval16{{start: 10, length: 5}, {start: 65000, length: 2000}}}
2612+
assert.ErrorIs(t, rc.validate(), ErrRunIntervalOutOfRange)
26042613
}
26052614

26062615
func TestIntervalValidationsPassing(t *testing.T) {

‎serialization_frozen_test.go‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,34 @@ func TestBitMapValidationFromFrozen(t *testing.T) {
151151
err error
152152
}{
153153
{
154-
name: "Corrupts Run Length vs Num Runs",
154+
name: "Last Run Extends Past Container End",
155155
loader: func(bm *Bitmap) {
156156
bm.AddRange(0, 2)
157157
bm.AddRange(4, 6)
158158
bm.AddRange(8, 100)
159+
bm.RunOptimize() // one run container
159160
},
160161
corruptor: func(s []byte) {
161-
// 21 is the length of the run of the last run/range
162-
// Shortening causes interval sum to be to short
163-
s[10] = 1
162+
// 10-11 is the length of the last run, which starts at 8:
163+
// 65535 makes it end past 65535
164+
s[10] = 0xFF
165+
s[11] = 0xFF
164166
},
165-
err: ErrRunIntervalSize,
167+
err: ErrRunIntervalOutOfRange,
166168
},
167169
{
168-
name: "Corrupts Run Length",
170+
name: "Run Extends Past Container End",
169171
loader: func(bm *Bitmap) {
170172
bm.AddRange(100, 110)
173+
bm.RunOptimize() // one run container
171174
},
172175
corruptor: func(s []byte) {
173-
s[2] = 0
176+
// 2-3 is the length of the run, which starts at 100:
177+
// 65535 makes it end past 65535
178+
s[2] = 0xFF
179+
s[3] = 0xFF
174180
},
175-
err: ErrRunIntervalSize,
181+
err: ErrRunIntervalOutOfRange,
176182
},
177183
{
178184
name: "Creates Interval Overlap",
@@ -219,7 +225,7 @@ func TestBitMapValidationFromFrozen(t *testing.T) {
219225
if tt.err == nil {
220226
assert.NoError(t, frozenViewErr, "expected MustFrozenView to succeed when tt.err is nil")
221227
} else {
222-
assert.Error(t, frozenViewErr, "expected MustFrozenView to fail when tt.err is not nil")
228+
assert.ErrorIs(t, frozenViewErr, tt.err)
223229
}
224230
})
225231
}

0 commit comments

Comments
 (0)