1212 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313 * See the License for the specific language governing permissions and
1414 * limitations under the License.
15+ * Portions Copyrighted 2026 3A Systems, LLC
1516 */
1617
1718package com .persistit ;
1819
20+ import com .persistit .exception .CorruptValueException ;
1921import com .persistit .exception .PersistitException ;
2022import com .persistit .util .Util ;
2123import junit .framework .Assert ;
2224import org .junit .Test ;
2325
26+ import java .util .ArrayList ;
2427import java .util .Map ;
2528import java .util .TreeMap ;
2629
3033import static org .junit .Assert .assertArrayEquals ;
3134import static org .junit .Assert .assertEquals ;
3235import static org .junit .Assert .assertTrue ;
36+ import static org .junit .Assert .fail ;
3337
3438public class MVVTest {
3539 @ Test
@@ -321,31 +325,31 @@ public void storeToExistingVersionAtCapacityShorterLength() {
321325 }
322326
323327 @ Test
324- public void fetchVersionFromUnused () {
328+ public void fetchVersionFromUnused () throws PersistitException {
325329 final long vh = 10 ;
326330 final byte [] source = {};
327331 final byte [] target = {};
328332 assertEquals (MVV .VERSION_NOT_FOUND , MVV .fetchVersion (source , -1 , vh , target ));
329333 }
330334
331335 @ Test
332- public void fetchVersionFromUndefined () {
336+ public void fetchVersionFromUndefined () throws PersistitException {
333337 final long vh = 10 ;
334338 final byte [] source = {};
335339 final byte [] target = {};
336340 assertEquals (MVV .VERSION_NOT_FOUND , MVV .fetchVersion (source , source .length , vh , target ));
337341 }
338342
339343 @ Test
340- public void fetchVersionFromPrimordial () {
344+ public void fetchVersionFromPrimordial () throws PersistitException {
341345 final long vh = 10 ;
342346 final byte [] source = { 0xA , 0xB , 0xC };
343347 final byte [] target = {};
344348 assertEquals (MVV .VERSION_NOT_FOUND , MVV .fetchVersion (source , source .length , vh , target ));
345349 }
346350
347351 @ Test
348- public void fetchVersionFromExistingNoFound () {
352+ public void fetchVersionFromExistingNoFound () throws PersistitException {
349353 final long vh = 10 ;
350354 final byte [] source = { (byte ) TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 3 , 0xA , 0xB , 0xC , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 2 ,
351355 0 , 2 , 0xD , 0xE };
@@ -354,7 +358,7 @@ public void fetchVersionFromExistingNoFound() {
354358 }
355359
356360 @ Test
357- public void fetchVersionFromExisting () {
361+ public void fetchVersionFromExisting () throws PersistitException {
358362 final long vh = 10 ;
359363 final byte [] source = { (byte ) TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 10 , 0 , 2 , 0xA , 0xB , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 11 , 0 ,
360364 3 , 0xB , 0xC };
@@ -366,7 +370,7 @@ public void fetchVersionFromExisting() {
366370 }
367371
368372 @ Test (expected = IllegalArgumentException .class )
369- public void fetchVersionFromExistingOverCapacity () {
373+ public void fetchVersionFromExistingOverCapacity () throws PersistitException {
370374 final long vh = 10 ;
371375 final byte [] source = { (byte ) TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 11 , 0 , 5 , 0x1 , 0x2 , 0x3 , 0x4 , 0x5 , 0 , 0 , 0 , 0 , 0 ,
372376 0 , 0 , 9 , 0 , 1 , 0xA , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 10 , 0 , 3 , 0xB , 0xC , 0xD };
@@ -429,21 +433,21 @@ public void visitAndFetchByOffsetMVV() throws PersistitException {
429433 }
430434
431435 @ Test (expected = IllegalArgumentException .class )
432- public void fetchByOffsetNegative () {
436+ public void fetchByOffsetNegative () throws PersistitException {
433437 final byte [] source = {};
434438 final byte [] target = new byte [10 ];
435439 MVV .fetchVersionByOffset (source , source .length , -1 , target );
436440 }
437441
438442 @ Test (expected = IllegalArgumentException .class )
439- public void fetchByOffsetTooLarge () {
443+ public void fetchByOffsetTooLarge () throws PersistitException {
440444 final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 3 , 0xA , 0xB , 0xC );
441445 final byte [] target = new byte [10 ];
442446 MVV .fetchVersionByOffset (source , source .length , source .length + 1 , target );
443447 }
444448
445449 @ Test
446- public void storeAndFetchVersionMany () {
450+ public void storeAndFetchVersionMany () throws PersistitException {
447451 final int VERSION_COUNT = 10 ;
448452 final int versions [] = new int [VERSION_COUNT ];
449453 final byte sources [][] = new byte [VERSION_COUNT ][];
@@ -480,6 +484,109 @@ public void tryValueTooLong() {
480484 MVV .storeVersion (target , 0 , 0 , target .length , VERSION , source , 0 , source .length );
481485 }
482486
487+ //
488+ // Issue #286: a version left in the marked state (e.g. by a prune that was
489+ // interrupted mid-way) must still be readable. The length accessors used
490+ // by the fetch paths formerly read the length field signed and with the
491+ // mark bit included, driving the scan offset negative and throwing
492+ // ArrayIndexOutOfBoundsException.
493+ //
494+
495+ @ Test
496+ public void visitMarkedVersion () throws PersistitException {
497+ final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 3 , 0xA , 0xB , 0xC , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 2 ,
498+ 0 , 2 , 0xD , 0xE );
499+ MVV .mark (source , 1 );
500+ final TestVisitor visitor = new TestVisitor ();
501+ MVV .visitAllVersions (visitor , source , 0 , source .length );
502+ assertTrue (visitor .initCalled );
503+ assertEquals (newVisitorMap (1 , 3 , 11 , 2 , 2 , 24 ), visitor .versions );
504+ }
505+
506+ @ Test
507+ public void fetchMarkedVersion () throws PersistitException {
508+ final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 10 , 0 , 2 , 0xA , 0xB , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 11 , 0 ,
509+ 3 , 0xC , 0xD , 0xE );
510+ MVV .mark (source , 1 );
511+ MVV .mark (source , 13 );
512+ final byte [] target = new byte [20 ];
513+ assertEquals (2 , MVV .fetchVersion (source , source .length , 10 , target ));
514+ assertArrayEqualsLen (newArray (0xA , 0xB ), target , 2 );
515+ assertEquals (3 , MVV .fetchVersion (source , source .length , 11 , target ));
516+ assertArrayEqualsLen (newArray (0xC , 0xD , 0xE ), target , 3 );
517+ }
518+
519+ @ Test
520+ public void fetchByOffsetMarkedVersion () throws PersistitException {
521+ final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 10 , 0 , 2 , 0xA , 0xB , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 11 , 0 ,
522+ 3 , 0xC , 0xD , 0xE );
523+ MVV .mark (source , 1 );
524+ final byte [] target = new byte [20 ];
525+ assertEquals (2 , MVV .fetchVersionByOffset (source , source .length , 11 , target ));
526+ assertArrayEqualsLen (newArray (0xA , 0xB ), target , 2 );
527+ }
528+
529+ @ Test (expected = CorruptValueException .class )
530+ public void visitTruncatedHeaderThrows () throws PersistitException {
531+ final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 );
532+ MVV .visitAllVersions (new TestVisitor (), source , 0 , source .length );
533+ }
534+
535+ /**
536+ * The old code also threw CorruptValueException here (from the trailing
537+ * length check), but only after handing the visitor an out-of-bounds
538+ * version — which Exchange.MvvVisitor would record and fetchVersionByOffset
539+ * would then copy. The fix must throw before the visitor sees it.
540+ */
541+ @ Test
542+ public void visitOverrunningLengthThrows () throws PersistitException {
543+ final byte [] source = newArray (TYPE_MVV , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 50 , 0xA , 0xB , 0xC );
544+ final TestVisitor visitor = new TestVisitor ();
545+ try {
546+ MVV .visitAllVersions (visitor , source , 0 , source .length );
547+ fail ("expected CorruptValueException" );
548+ } catch (final CorruptValueException expected ) {
549+ }
550+ assertTrue ("visitor must not see an out-of-bounds version" , visitor .versions .isEmpty ());
551+ }
552+
553+ /**
554+ * Issue #286: when prune exits via an exception after its first pass has
555+ * marked versions (e.g. interrupted while resolving a commit status, or a
556+ * CorruptValueException), the finally-block safety net must remove the
557+ * marks. Its loop bound was wrong for a non-zero offset — exactly the
558+ * in-page pruning case — leaving mark bits behind in the live buffer page.
559+ */
560+ @ Test
561+ public void pruneExceptionUnmarksVersionsAtNonZeroOffset () throws Exception {
562+ final TimestampAllocator tsa = new TimestampAllocator ();
563+ final TransactionIndex ti = new TransactionIndex (tsa , 1 );
564+ final TransactionStatus status1 = ti .registerTransaction ();
565+ final TransactionStatus status2 = ti .registerTransaction ();
566+
567+ /*
568+ * Two uncommitted versions from different transactions make prune's
569+ * first pass throw "Multiple uncommitted versions" after it has marked
570+ * the first version. The non-zero offset emulates in-page pruning.
571+ */
572+ final int offset = 117 ;
573+ final byte [] bytes = new byte [offset + 100 ];
574+ final byte [] v1 = { 0xA , 0xB , 0xC };
575+ final byte [] v2 = { 0xD , 0xE };
576+ int length = MVV .storeVersion (bytes , offset , -1 , bytes .length , TransactionIndex .ts2vh (status1 .getTs ()), v1 , 0 ,
577+ v1 .length ) & STORE_LENGTH_MASK ;
578+ length = MVV .storeVersion (bytes , offset , length , bytes .length , TransactionIndex .ts2vh (status2 .getTs ()), v2 , 0 ,
579+ v2 .length ) & STORE_LENGTH_MASK ;
580+
581+ final byte [] before = bytes .clone ();
582+ try {
583+ MVV .prune (bytes , offset , length , ti , true , new ArrayList <MVV .PrunedVersion >());
584+ fail ("expected CorruptValueException" );
585+ } catch (final CorruptValueException expected ) {
586+ }
587+ assertArrayEquals ("prune must leave the byte array unchanged when it throws" , before , bytes );
588+ }
589+
483590 //
484591 // Test helper methods
485592 //
0 commit comments