PdfXrefTableUnitTest.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf;

import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.MemoryLimitsAwareException;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.test.AssertUtil;
import com.itextpdf.test.ExceptionTestUtil;
import com.itextpdf.test.ExtendedITextTest;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

@Tag("UnitTest")
public class PdfXrefTableUnitTest extends ExtendedITextTest {
    @Test
    public void checkNumberOfIndirectObjectsTest() {
        PdfXrefTable table = new PdfXrefTable();
        Assertions.assertEquals(0, table.getCountOfIndirectObjects());

        int numberOfReferences = 10;

        for (int i = 0; i < numberOfReferences; i++) {
            table.add(new PdfIndirectReference(null, i + 1));
        }

        Assertions.assertEquals(numberOfReferences, table.getCountOfIndirectObjects());
    }

    @Test
    public void checkNumberOfIndirectObjectsWithFreeReferencesTest() {
        PdfXrefTable table = new PdfXrefTable();

        int numberOfReferences = 10;

        for (int i = 0; i < numberOfReferences; i++) {
            table.add(new PdfIndirectReference(null, i + 1));
        }

        table.initFreeReferencesList(null);

        int freeReferenceNumber = 5;
        table.freeReference(table.get(freeReferenceNumber));

        Assertions.assertEquals(numberOfReferences - 1, table.getCountOfIndirectObjects());
        Assertions.assertTrue(table.get(freeReferenceNumber).isFree());
    }

    @Test
    public void checkNumberOfIndirectObjectsWithRandomNumbersTest() {
        PdfXrefTable table = new PdfXrefTable();

        int numberOfReferences = 10;

        for (int i = 0; i < numberOfReferences; i++) {
            table.add(new PdfIndirectReference(null, i * 25));
        }

        Assertions.assertEquals(numberOfReferences, table.getCountOfIndirectObjects());
        Assertions.assertEquals(226, table.size());
    }

    @Test
    public void checkExceedTheNumberOfElementsInXrefTest() {
        final MemoryLimitsAwareHandler memoryLimitsAwareHandler = new MemoryLimitsAwareHandler();
        memoryLimitsAwareHandler.setMaxNumberOfElementsInXrefStructure(5);

        final PdfXrefTable xrefTable = new PdfXrefTable(5, memoryLimitsAwareHandler);
        final int numberOfReferences = 5;
        for (int i = 1; i < numberOfReferences; i++) {
            xrefTable.add(new PdfIndirectReference(null, i));
        }

        Exception exception = Assertions.assertThrows(MemoryLimitsAwareException.class,
                () -> xrefTable.add(new PdfIndirectReference(null, numberOfReferences)));
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_STRUCTURE_SIZE_EXCEEDED_THE_LIMIT,
                exception.getMessage());
    }

    @Test
    public void ensureCapacityExceedTheLimitTest() {
        final MemoryLimitsAwareHandler memoryLimitsAwareHandler = new MemoryLimitsAwareHandler();
        final PdfXrefTable xrefTable = new PdfXrefTable(memoryLimitsAwareHandler);
        final int newCapacityExceededTheLimit = memoryLimitsAwareHandler.getMaxNumberOfElementsInXrefStructure() + 2;

        // There we add 2 instead of 1 since xref structures used 1-based indexes, so we decrement the capacity
        // before check.
        Exception ex = Assertions.assertThrows(MemoryLimitsAwareException.class,
                () -> xrefTable.setCapacity(newCapacityExceededTheLimit));
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_STRUCTURE_SIZE_EXCEEDED_THE_LIMIT, ex.getMessage());
    }

    @Test
    public void passCapacityGreaterThanLimitInConstructorTest() {
        final MemoryLimitsAwareHandler memoryLimitsAwareHandler = new MemoryLimitsAwareHandler();
        memoryLimitsAwareHandler.setMaxNumberOfElementsInXrefStructure(20);

        Exception ex = Assertions.assertThrows(MemoryLimitsAwareException.class,
                () -> new PdfXrefTable(30, memoryLimitsAwareHandler));
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_STRUCTURE_SIZE_EXCEEDED_THE_LIMIT, ex.getMessage());
    }

    @Test
    public void zeroCapacityInConstructorWithHandlerTest() {
        final MemoryLimitsAwareHandler memoryLimitsAwareHandler = new MemoryLimitsAwareHandler();
        memoryLimitsAwareHandler.setMaxNumberOfElementsInXrefStructure(20);
        final PdfXrefTable xrefTable = new PdfXrefTable(0, memoryLimitsAwareHandler);

        Assertions.assertEquals(20, xrefTable.getCapacity());
    }

    @Test
    public void xRefMaxValueLong() {
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        document.xref.add(new PdfIndirectReferenceProxy(document, 11, Long.MAX_VALUE));

        Exception e = Assertions.assertThrows(PdfException.class, () -> {
            document.close();
        });
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_HAS_AN_ENTRY_WITH_TOO_BIG_OFFSET, e.getMessage());
    }


    @Test
    public void maxCrossReferenceOffSetReached() {
        long justOver10gbLogical = 10_000_000_001L;
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        document.xref.add(new PdfIndirectReferenceProxy(document, 11, justOver10gbLogical));

        Exception e = Assertions.assertThrows(PdfException.class, () -> {
            document.close();
        });
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_HAS_AN_ENTRY_WITH_TOO_BIG_OFFSET, e.getMessage());
    }

    @Test
    public void maxCrossReference() {
        long justOver10gbLogical = 10_000_000_000L;
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        document.xref.add(new PdfIndirectReferenceProxy(document, 11, justOver10gbLogical));

        Exception e = Assertions.assertThrows(PdfException.class, () -> {
            document.close();
        });
        Assertions.assertEquals(KernelExceptionMessageConstant.XREF_HAS_AN_ENTRY_WITH_TOO_BIG_OFFSET, e.getMessage());
    }

    @Test
    public void justBelowXrefThreshold() {
        long maxAllowedOffset = 10_000_000_000L - 1L;
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        document.xref.add(new PdfIndirectReferenceProxy(document, 11, maxAllowedOffset));

        AssertUtil.doesNotThrow(() -> document.close());
    }

    @Test
    public void xRefIntMax() {
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        document.xref.add(new PdfIndirectReferenceProxy(document, 11, Integer.MAX_VALUE));
        AssertUtil.doesNotThrow(() -> document.close());
    }




}
 class PdfIndirectReferenceProxy extends PdfIndirectReference {
    private final long offset;

    public PdfIndirectReferenceProxy(PdfDocument document, int objNumber, long offset) {
        super(document, objNumber);
        this.offset = offset;
    }

    @Override
    public long getOffset() {
        return offset;
    }
}