PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Exception

Unsupported PPS type

Error message

Unsupported PPS type

What it means

Thrown while Shared\OLE parses the property-set (PPS) directory of a compound document: each entry's type field must be 1 (directory), 2 (file/stream) or 5 (root), and anything else hits the default branch that raises this exception. An out-of-range type byte means the directory structure is malformed.

Source

Thrown at src/PhpSpreadsheet/Shared/OLE.php:321

            // Simple conversion from UTF-16LE to ISO-8859-1
            $name = str_replace("\x00", '', $nameUtf16);
            $type = self::readInt1($fh);
            switch ($type) {
                case self::OLE_PPS_TYPE_ROOT:
                    $pps = new Root(null, null, []);
                    $this->root = $pps;

                    break;
                case self::OLE_PPS_TYPE_DIR:
                    $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []);

                    break;
                case self::OLE_PPS_TYPE_FILE:
                    $pps = new OLE\PPS\File($name);

                    break;
                default:
                    throw new Exception('Unsupported PPS type');
            }
            fseek($fh, 1, SEEK_CUR);
            $pps->Type = $type;
            $pps->Name = $name;
            $pps->PrevPps = self::readInt4($fh);
            $pps->NextPps = self::readInt4($fh);
            $pps->DirPps = self::readInt4($fh);
            fseek($fh, 20, SEEK_CUR);
            $pps->Time1st = self::OLE2LocalDate((string) fread($fh, 8));
            $pps->Time2nd = self::OLE2LocalDate((string) fread($fh, 8));
            $pps->startBlock = self::readInt4($fh);
            $pps->Size = self::readInt4($fh);
            $pps->No = count($this->_list);
            $this->_list[] = $pps;

            // check if the PPS tree (starting from root) is complete
            if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) {
                break;

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Re-acquire the file from its source and retry; verify with another reader (Excel/LibreOffice) that the workbook itself opens.
  2. Validate uploads by content sniffing plus a size sanity check before invoking spreadsheet parsing, so obviously malformed files are rejected earlier.
  3. Catch PhpSpreadsheet\Exception around load and convert to a user-facing 'unsupported or damaged file' result rather than a 500.
  4. Keep the library current — hardening for malformed OLE structures lands in patch releases.

Example fix

// before
$spreadsheet = (new \PhpOffice\PhpSpreadsheet\Reader\Xls())->load('batch.xls');
// Unsupported PPS type

// after
try {
    $spreadsheet = (new \PhpOffice\PhpSpreadsheet\Reader\Xls())->load('batch.xls');
} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    logger()->warning('Rejecting malformed workbook', ['file' => 'batch.xls', 'err' => $e->getMessage()]);
    return response()->unprocessableEntity('The uploaded workbook is damaged or unsupported.');
}
Defensive patterns

Strategy: try-catch

Validate before calling

$head = (string) @file_get_contents($file, false, null, 0, 8);
if ($head !== "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1") {
    throw new RuntimeException('Not a legacy .xls workbook');
}

Try / catch

try { $spreadsheet = $reader->load($file); }
catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    if (in_array($e->getMessage(), ['Unsupported PPS type', 'Detected loop while iterating blocks', 'Expecting 8 byte string'], true)
        || str_contains($e->getMessage(), 'invalid.')) {
        return ['error' => 'The workbook file is corrupt. Please re-export and re-upload.'];
    }
    throw $e;
}

Prevention

When it happens

Trigger: OLE::read() iterating PPS entries (in _readPpsWks) over a .xls whose directory stream is damaged, so the byte interpreted as the type is garbage (0, or >5). Reached only after the OLE signature and FAT parsing succeeded, so the container header was plausible but the directory is corrupt.

Common situations: Truncated downloads where the directory sector is incomplete; files damaged by antivirus sanitization or partial writes; malformed .xls produced by buggy third-party generators; fuzzed/hostile uploads designed to crash parsers.

Related errors


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/2c4dd86c5b16251e. Report an issue: GitHub.