IO-Compress

 view release on metacpan or  search on metacpan

bin/zipdetails  view on Meta::CPAN

    my $foundCentralHeader = 0;
    my $lastEndsAt = 0;
    my $lastSignature = 0;
    my $lastHeader = {};

    $CentralDirectory->{alreadyScanned} = 1 ;

    my $output_encryptedCD = 0;

    reportPrefixData();
    while(my $s = scanForSignature($opt_walk))
    {
        my $here = $FH->tell();
        my $delta = $here - $lastEndsAt ;

        # delta can only be negative when '--scan' is used
        if ($delta < 0 )
        {
            # nested or overlap
            # check if nested
            # remember & check if matching entry in CD
            # printf("### WARNING: OVERLAP/NESTED Record found 0x%X 0x%X $delta\n", $here, $lastEndsAt) ;
        }
        elsif ($here != $lastEndsAt)
        {
            # scanForSignature had to skip bytes to find the next signature

            # some special cases that don't have signatures need to be checked first

            seekTo($lastEndsAt);

            if (! $output_encryptedCD && $CentralDirectory->isEncryptedCD())
            {
                displayEncryptedCD();
                $output_encryptedCD = 1;
                $lastEndsAt = $FH->tell();
                next;
            }
            elsif ($lastSignature == ZIP_LOCAL_HDR_SIG && $lastHeader->{'streamed'} )
            {
                # Check for size of possible malformed Data Descriptor before outputting payload
                if (! $lastHeader->{'gotDataDescriptorSize'})
                {
                    my $hdrSize = checkForBadlyFormedDataDescriptor($lastHeader, $delta) ;

                    if ($hdrSize)
                    {
                        # remove size of Data Descriptor from payload
                        $delta -= $hdrSize;
                        $lastHeader->{'gotDataDescriptorSize'} = $hdrSize;
                    }
                }

                if(defined($lastHeader->{'payloadOutput'}) && ($lastEndsAt = BadlyFormedDataDescriptor($lastHeader, $delta)))
                {
                    $HeaderOffsetIndex->rewindIndex();
                    $lastHeader->{entry}->readDataDescriptor(1) ;
                    next;
                }

                # Assume we have the payload when streaming is enabled
                outSomeData($delta, "PAYLOAD", $opt_Redact) ;
                $lastHeader->{'payloadOutput'} = 1;
                $lastEndsAt = $FH->tell();

                next;
            }
            elsif (Signatures::isCentralHeader($s) && $foundCentralHeader == 0)
            {
                # check for an APK header directly before the first Central Header
                $foundCentralHeader = 1;

                ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here, 0) ;

                if ($START_APK)
                {
                    seekTo($lastEndsAt+4);

                    scanApkBlock();
                    $lastEndsAt = $FH->tell();
                    next;
                }

                seekTo($lastEndsAt);
            }

            # Not a special case, so output generic padding message
            if ($delta > 0)
            {
                reportPrefixData($delta)
                    if $lastEndsAt == 0 ;
                outSomeDataParagraph($delta, "UNEXPECTED PADDING");
                info  $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
                    if $FH->tell() - $delta ;
                $POSSIBLE_PREFIX_DELTA = $delta
                    if $lastEndsAt ==  0;
                $lastEndsAt = $FH->tell();
                next;
            }
            else
            {
                seekTo($here);
            }

        }

        my ($buffer, $signature) = read_V();

        $lastSignature = $signature;

        my $handler = Signatures::decoder($signature);
        if (!defined $handler) {
            internalFatal undef, "xxx";
        }

        $foundZipRecords = 1;
        $lastHeader = $handler->($signature, $buffer, $FH->tell() - 4) // {'streamed' => 0};

        $lastEndsAt = $FH->tell();

        seekTo($here + 4)

bin/zipdetails  view on Meta::CPAN

        }

        my ($buffer, $signature) = read_V();

        $expectedOffset = undef;
        $expectedSignature = undef;

        # Check for split archive marker at start of file
        if ($here == 0 && $signature == ZIP_SINGLE_SEGMENT_MARKER)
        {
            #  let it drop through
            $expectedSignature = ZIP_SINGLE_SEGMENT_MARKER;
            $expectedOffset = 0;
        }
        else
        {
            my $expectedEntry = $HeaderOffsetIndex->getNextIndex() ;
            if ($expectedEntry)
            {
                $expectedOffset = $expectedEntry->offset();
                $expectedSignature = $expectedEntry->signature();
                $expectedBuffer = pack "V", $expectedSignature ;
            }
        }

        my $delta = $expectedOffset - $here ;

        # if ($here != $expectedOffset && $signature != ZIP_DATA_HDR_SIG)
        # {
        #     rewindRelative(4);
        #     my $delta = $expectedOffset - $here ;
        #     outSomeDataParagraph($delta, "UNEXPECTED PADDING");
        #     $HeaderOffsetIndex->rewindIndex();
        #     next;
        # }

        # Need to check for use-case where
        # * there is a ZIP_DATA_HDR_SIG directly after a ZIP_LOCAL_HDR_SIG.
        #   The HeaderOffsetIndex object doesn't have visibility of it.
        # * APK header directly before the CD
        # * zipbomb

        if (defined $expectedOffset && $here != $expectedOffset && ( $CentralDirectory->exists() || $EOCD_Present) )
        {
            if ($here > $expectedOffset)
            {
                # Probable zipbomb

                # Cursor $OFFSET need to rewind
                $OFFSET = $expectedOffset;
                $FH->seek($OFFSET + 4, SEEK_SET) ;

                $signature = $expectedSignature;
                $buffer = $expectedBuffer ;
            }

            # If get here then $here is less than $expectedOffset


            # check for an APK header directly before the first Central Header
            # Make sure not to miss a streaming data descriptor
            if ($signature != ZIP_DATA_HDR_SIG && Signatures::isCentralHeader($expectedSignature) && $START_APK && ! $processedAPK )
            {
                seekTo($here+4);
                # rewindRelative(4);
                scanApkBlock();
                $HeaderOffsetIndex->rewindIndex();
                $processedAPK = 1;
                next;
            }

            # Check Encrypted Central Directory
            # if ($CentralHeaderSignatures{$expectedSignature} && $CentralDirectory->isEncryptedCD() && ! $processedECD)
            # {
            #     # rewind the invalid signature
            #     seekTo($here);
            #     # rewindRelative(4);
            #     displayEncryptedCD();
            #     $processedECD = 1;
            #     next;
            # }

            if ($signature != ZIP_DATA_HDR_SIG && $delta >= 0)
            {
                rewindRelative(4);
                if($lastHeader->{'streamed'} && BadlyFormedDataDescriptor($lastHeader, $delta))
                {
                    $lastHeader->{entry}->readDataDescriptor(1) ;
                    $HeaderOffsetIndex->rewindIndex();
                    next;
                }

                reportPrefixData($delta)
                    if $here == 0;
                outSomeDataParagraph($delta, "UNEXPECTED PADDING");
                info  $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
                    if $FH->tell() - $delta ;
                $HeaderOffsetIndex->rewindIndex();
                next;
            }

            # ZIP_DATA_HDR_SIG drops through
        }

        my $handler = Signatures::decoder($signature);

        if (!defined $handler)
        {
            # if ($CentralDirectory->exists()) {

            #     # Should be at offset that central directory says
            #     my $locOffset = $CentralDirectory->getNextLocalOffset();
            #     my $delta = $locOffset - $here ;

            #     if ($here + 4 == $locOffset ) {
            #         for (0 .. 3) {
            #             $FH->ungetc(ord(substr($buffer, $_, 1)))
            #         }
            #         outSomeData($delta, "UNEXPECTED PADDING");
            #         next;
            #     }

bin/zipdetails  view on Meta::CPAN

                # Central Directory knows about these, so this is a zipbomb

                error undef, "Possible zipbomb -- Nested Local Entries found";
                for my $loc (sort keys %bomb)
                {
                    my $count = scalar @{ $bomb{$loc} };
                    my $outerEntry = $LocalDirectory->getByLocalOffset($loc);
                    say "# Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) .  " has $count nested Local Headers";

                    my $table = new SimpleTable;
                    $table->addHeaderRow('Offset', 'Filename');
                    $table->addDataRow(decimalHex0x($_), $LocalDirectory->getByLocalOffset($_)->outputFilename)
                        for sort @{ $bomb{$loc} } ;

                    $table->display();

                    delete $cleanCentralEntries{ $_ }
                        for grep { defined $_ }
                            map  { $CentralDirectory->{byLocalOffset}{$_}{centralHeaderOffset} }
                            @{ $bomb{$loc} } ;
                }
            }
        }

        # Check if contents of local headers match with Central Headers
        #
        # When Central Header encryption is used the local header values are masked (see APPNOTE 6.3.10, sec 4)
        # In this usecase the Central Header will appear to be absent
        #
        # key fields
        #    filename, compressed/uncompressed lengths, crc, compression method
        {
            for my $centralEntry ( sort { $a->centralHeaderOffset() <=> $b->centralHeaderOffset() } values %cleanCentralEntries )
            {
                my $localOffset = $centralEntry->localHeaderOffset;
                my $localEntry = $LocalDirectory->getByLocalOffset($localOffset);

                next
                    unless $localEntry;

                state $fields = [
                    # field name         offset    display name         stringify
                    ['filename',            ZIP_CD_FILENAME_OFFSET,
                                                'Filename',             undef, ],
                    ['extractVersion',       7, 'Extract Zip Spec',     sub { decimalHex0xUndef($_[0]) . " " . decodeZipVer($_[0]) }, ],
                    ['generalPurposeFlags',  8, 'General Purpose Flag', \&decimalHex0xUndef, ],
                    ['compressedMethod',    10, 'Compression Method',   sub { decimalHex0xUndef($_[0]) . " " . getcompressionMethodName($_[0]) }, ],
                    ['lastModDateTime',     12, 'Modification Time',    sub { decimalHex0xUndef($_[0]) . " " . LastModTime($_[0]) }, ],
                    ['crc32',               16, 'CRC32',                \&decimalHex0xUndef, ],
                    ['compressedSize',      20, 'Compressed Size',      \&decimalHex0xUndef, ],
                    ['uncompressedSize',    24, 'Uncompressed Size',    \&decimalHex0xUndef, ],

                ] ;

                my $table = new SimpleTable;
                $table->addHeaderRow('Field Name', 'Central Offset', 'Central Value', 'Local Offset', 'Local Value');

                for my $data (@$fields)
                {
                    my ($field, $offset, $name, $stringify) = @$data;
                    # if the local header uses streaming and we are running a scan/walk, the compressed/uncompressed sizes will not be known
                    my $localValue = $localEntry->{$field} ;
                    my $centralValue = $centralEntry->{$field};

                    if (($localValue // '-1') ne ($centralValue // '-2'))
                    {
                        if ($stringify)
                        {
                            $localValue = $stringify->($localValue);
                            $centralValue = $stringify->($centralValue);
                        }

                        $table->addDataRow($name,
                                            decimalHex0xUndef($centralEntry->centralHeaderOffset() + $offset),
                                            $centralValue,
                                            decimalHex0xUndef($localOffset+$offset),
                                            $localValue);
                    }
                }

                my $badFields = $table->hasData;
                if ($badFields)
                {
                    error undef, "Found $badFields Field Mismatch for Filename '". $centralEntry->outputFilename . "'";
                    $table->display();
                }
            }
        }

    }
    elsif ($CentralDirectory->exists())
    {
        my @messages = "Central Directory exists, but Local Directory not found" ;
        push @messages , "Try running with --walk' or '--scan' options"
            unless $opt_scan || $opt_walk ;
        error undef, @messages;
    }
    elsif ($LocalDirectory->exists())
    {
        if ($CentralDirectory->isEncryptedCD())
        {
            warning undef, "Local Directory exists, but Central Directory is encrypted"
        }
        else
        {
            error undef, "Local Directory exists, but Central Directory not found"
        }

    }

    if ($ErrorCount ||$WarningCount || $InfoCount )
    {
        say "#"
            unless $lastWasMessage ;

        say "# Error Count: $ErrorCount"
            if $ErrorCount;
        say "# Warning Count: $WarningCount"
            if $WarningCount;
        say "# Info Count: $InfoCount"
            if $InfoCount;

bin/zipdetails  view on Meta::CPAN


sub compressionMethod
{
    my $id = shift ;
    Value_v($id) . getcompressionMethodName($id);
}

sub LocalHeader
{
    my $signature = shift ;
    my $data = shift ;
    my $startRecordOffset = shift ;

    my $locHeaderOffset = $FH->tell() -4 ;

    ++ $LocalHeaderCount;
    print "\n";
    out $data, "LOCAL HEADER #$LocalHeaderCount" , Value_V($signature);

    need 26, Signatures::name($signature);

    my $buffer;
    my $orphan = 0;

    my ($loc, $CDcompressedSize, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) ;
    my $CentralEntryExists = $CentralDirectory->localOffset($startRecordOffset);
    my $localEntry = LocalDirectoryEntry->new();

    my $cdEntry;

    if (! $opt_scan && ! $opt_walk && $CentralEntryExists)
    {
        $cdEntry = $CentralDirectory->getByLocalOffset($startRecordOffset);

        if (! $cdEntry)
        {
            out1 "Orphan Entry: No matching central directory" ;
            $orphan = 1 ;
        }

        $cdZip64 = $cdEntry->zip64ExtraPresent;
        $zip64Sizes = $cdEntry->zip64SizesPresent;
        $cdEntryOffset = $cdEntry->centralHeaderOffset ;
        $localEntry->addCdEntry($cdEntry) ;

        if ($cdIndex && $cdIndex != $LocalHeaderCount)
        {
            # fatal undef, "$cdIndex != $LocalHeaderCount"
        }
    }

    my $extractVer = out_C  "Extract Zip Spec", \&decodeZipVer;
    out_C  "Extract OS", \&decodeOS;

    my ($bgp, $gpFlag) = read_v();
    my ($bcm, $compressedMethod) = read_v();

    out $bgp, "General Purpose Flag", Value_v($gpFlag) ;
    GeneralPurposeBits($compressedMethod, $gpFlag);
    my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ;
    my $streaming = $gpFlag & ZIP_GP_FLAG_STREAMING_MASK ;
    $localEntry->languageEncodingFlag($LanguageEncodingFlag) ;

    out $bcm, "Compression Method",   compressionMethod($compressedMethod) ;
    info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2)
        if ! defined $ZIP_CompressionMethods{$compressedMethod} ;

    my $lastMod = out_V "Modification Time", sub { LastModTime($_[0]) };

    my $crc              = out_V "CRC";

    # Weak encryption if
    # * encrypt flag (bit 0) set in General Purpose Flags
    # * strong encrypt (bit ) not set in General Purpose Flags
    # * not using AES encryption (compression method 99)
    my $weakEncryption = ($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ENCRYPTED_MASK &&
                         $compressedMethod != ZIP_CM_AES;
    # Weak encryption uses the CRC value even when streaming is in play.
    # This conflicts with appnote 6.3.10 section 4.4.4
    warning $FH->tell() - 4, "CRC field should be zero when streaming is enabled"
        if $streaming && $crc != 0 && ! $weakEncryption;

    my $compressedSize   = out_V "Compressed Size";
    # warning $FH->tell(), "Compressed Size should be zero when streaming is enabled";

    my $uncompressedSize = out_V "Uncompressed Size";
    # warning $FH->tell(), "Uncompressed Size should be zero when streaming is enabled";

    my $filenameLength   = out_v "Filename Length";

    if ($filenameLength == 0)
    {
        info $FH->tell()- 2, "Zero Length filename";
    }

    my $extraLength        = out_v "Extra Length";

    my $filename = '';
    if ($filenameLength)
    {
        need $filenameLength, Signatures::name($signature), 'Filename';

        myRead(my $raw_filename, $filenameLength);
        $localEntry->filename($raw_filename) ;
        $filename = outputFilename($raw_filename, $LanguageEncodingFlag);
        $localEntry->outputFilename($filename);
    }

    $localEntry->localHeaderOffset($locHeaderOffset) ;
    $localEntry->offsetStart($locHeaderOffset) ;
    $localEntry->compressedSize($compressedSize) ;
    $localEntry->uncompressedSize($uncompressedSize) ;
    $localEntry->extractVersion($extractVer);
    $localEntry->generalPurposeFlags($gpFlag);
    $localEntry->lastModDateTime($lastMod);
    $localEntry->crc32($crc) ;
    $localEntry->zip64ExtraPresent($cdZip64) ;
    $localEntry->zip64SizesPresent($zip64Sizes) ;

    $localEntry->compressedMethod($compressedMethod) ;
    $localEntry->streamed($gpFlag & ZIP_GP_FLAG_STREAMING_MASK) ;

    $localEntry->std_localHeaderOffset($locHeaderOffset + $PREFIX_DELTA) ;
    $localEntry->std_compressedSize($compressedSize) ;
    $localEntry->std_uncompressedSize($uncompressedSize) ;
    $localEntry->std_diskNumber(0) ;

    if ($extraLength)
    {
        need $extraLength, Signatures::name($signature), 'Extra';
        walkExtra($extraLength, $localEntry);
    }

    # Defer test for directory payload until Central Header processing.
    # Need to have external file attributes to deal with sme edge conditions.
    # # APPNOTE 6.3.10, sec 4.3.8
    # warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload"
    #     if ! $streaming && $filename =~ m#/$# && $localEntry->uncompressedSize ;

    my @msg ;
    # if ($cdZip64 && ! $ZIP64)
    # {
    #     # Central directory said this was Zip64
    #     # some zip files don't have the Zip64 field in the local header
    #     # seems to be a streaming issue.
    #     push @msg, "Missing Zip64 extra field in Local Header #$hexHdrCount\n";

    #     if (! $zip64Sizes)
    #     {
    #         # Central has a ZIP64 entry that doesn't have sizes
    #         # Local doesn't have a Zip 64 at all
    #         push @msg, "Unzip may complain about 'overlapped components' #$hexHdrCount\n";
    #     }
    #     else
    #     {
    #         $ZIP64 = 1
    #     }
    # }


    my $minizip_encrypted = $localEntry->minizip_secure;
    my $pk_encrypted      = ($gpFlag & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK) && $compressedMethod != ZIP_CM_AES && ! $minizip_encrypted;

    # Detecting PK strong encryption from a local header is a bit convoluted.
    # Cannot just use ZIP_GP_FLAG_ENCRYPTED_CD because minizip also uses this bit.
    # so jump through some hoops
    #     extract ver is >= 5.0'
    #     all the encryption flags are set in gpflags
    #     TODO - add zero lengths for crc, compressed & uncompressed

    if (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ALL_ENCRYPT  && $extractVer >= 0x32  )
    {
        $CentralDirectory->setPkEncryptedCD()
    }

    my $size = 0;

    # If no CD scanned, get compressed Size from local header.
    # Zip64 extra field takes priority
    my $cdl = defined $cdEntry
                ? $cdEntry->compressedSize()
                : undef;

    $CDcompressedSize = $localEntry->compressedSize ;
    $CDcompressedSize = $cdl
        if defined $cdl && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;

    my $cdu = defined $CentralDirectory->{byLocalOffset}{$locHeaderOffset}
                ? $CentralDirectory->{byLocalOffset}{$locHeaderOffset}{uncompressedSize}
                : undef;
    my $CDuncompressedSize = $localEntry->uncompressedSize ;

    $CDuncompressedSize = $cdu
        if defined $cdu && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;

    my $fullCompressedSize = $CDcompressedSize;

    my $payloadOffset = $FH->tell();
    $localEntry->payloadOffset($payloadOffset) ;
    $localEntry->offsetEnd($payloadOffset + $fullCompressedSize -1) ;

    if ($CDcompressedSize)
    {
        # check if enough left in file for the payload
        my $available = $FILELEN - $FH->tell;

bin/zipdetails  view on Meta::CPAN

sub DataDescriptor
{

    # Data header record or Spanned archive marker.
    #

    # ZIP_DATA_HDR_SIG at start of file flags a spanned zip file.
    # If it is a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG
    # See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5

    # If not at start of file, assume a Data Header Record
    # See APPNOTE 6.3.10, sec 4.3.9 & 4.3.9.3

    my $signature = shift ;
    my $data = shift ;
    my $startRecordOffset = shift ;

    my $here = $FH->tell();

    if ($here == 4)
    {
        # Spanned Archive Marker
        out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
        return;

        # my (undef, $next_sig) = read_V();
        # seekTo(0);

        # if ($next_sig == ZIP_LOCAL_HDR_SIG)
        # {
        #     print "\n";
        #     out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
        #     seekTo($here);
        #     return;
        # }
    }

    my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG);

    print "\n";
    out $data, $sigName, Value_V($signature);

    need  24, Signatures::name($signature);

    # Ignore header payload if nested (assume 64-bit descriptor)
    if (Nesting::isNested( $here - 4, $here - 4 + 24 - 1))
    {
        out "",  "Skipping Nested Payload";
        return {};
    }

    my $compressedSize;
    my $uncompressedSize;

    my $localEntry = $LocalDirectory->lastStreamedEntryAdded();
    my $centralEntry =  $localEntry && $localEntry->getCdEntry ;

    if (!$localEntry)
    {
        # found a Data Descriptor without a local header
        out "",  "Skipping Data Descriptor", "No matching Local header with streaming bit set";
        error $here - 4, "Orphan '$sigName' found", "No matching Local header with streaming bit set";
        return {};
    }

    my $crc = out_V "CRC";
    my $payloadLength = $here - 4 - $localEntry->payloadOffset;

    my $deltaToNext = deltaToNextSignature();
    my $cl32 = unpack "V",  peekAtOffset($here + 4, 4);
    my $cl64 = unpack "Q<", peekAtOffset($here + 4, 8);

    # use delta to next header & payload length
    # deals with use case where the payload length < 32 bit
    # will use a 32-bit value rather than the 64-bit value

    # see if delta & payload size match
    if ($deltaToNext == 16 && $cl64 == $payloadLength)
    {
        if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
        {
            error $here, "'$sigName': expected 32-bit values, got 64-bit";
        }

        $compressedSize   = out_Q "Compressed Size" ;
        $uncompressedSize = out_Q "Uncompressed Size" ;
    }
    elsif ($deltaToNext == 8 && $cl32 == $payloadLength)
    {
        if ($localEntry->zip64)
        {
            error $here, "'$sigName': expected 64-bit values, got 32-bit";
        }

        $compressedSize   = out_V "Compressed Size" ;
        $uncompressedSize = out_V "Uncompressed Size" ;
    }

    # Try matching just payload lengths
    elsif ($cl32 == $payloadLength)
    {
        if ($localEntry->zip64)
        {
            error $here, "'$sigName': expected 64-bit values, got 32-bit";
        }

        $compressedSize   = out_V "Compressed Size" ;
        $uncompressedSize = out_V "Uncompressed Size" ;

        warning $here, "'$sigName': Zip Header not directly after Data Descriptor";
    }
    elsif ($cl64 == $payloadLength)
    {
        if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
        {
            error $here, "'$sigName': expected 32-bit values, got 64-bit";
        }

        $compressedSize   = out_Q "Compressed Size" ;
        $uncompressedSize = out_Q "Uncompressed Size" ;

        warning $here, "'$sigName': Zip Header not directly after Data Descriptor";



( run in 0.974 second using v1.01-cache-2.11-cpan-39bf76dae61 )