Update 7z support

This commit is contained in:
DariusIII
2025-09-02 17:00:36 +02:00
parent 69cc7a9c63
commit 923fe5cba7
7 changed files with 965 additions and 176 deletions
+1
View File
@@ -117,6 +117,7 @@ ECHOCLI=true
RENAME_PAR2=false RENAME_PAR2=false
ADD_PAR2=false ADD_PAR2=false
RENAME_MUSIC_MEDIAINFO=true RENAME_MUSIC_MEDIAINFO=true
FETCH_LAST_FILE=true
CACHE_EXPIRY_SHORT=5 CACHE_EXPIRY_SHORT=5
CACHE_EXPIRY_MEDIUM=10 CACHE_EXPIRY_MEDIUM=10
CACHE_EXPIRY_LONG=15 CACHE_EXPIRY_LONG=15
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,287 @@
<?php
namespace Blacklight\processing\post;
/**
* Minimal partial 7z header parser to recover file names from an in-memory buffer.
* Supports unencoded headers directly; encoded headers are flagged so caller can fallback
* to external 7z listing for sizes/attributes. Provides heuristics for encryption.
*/
class SevenZipPartialParser
{
private string $data;
private int $len;
private array $names = [];
private bool $parsed = false;
private bool $encodedHeader = false; // flag if we encountered kEncodedHeader
private bool $encrypted = false; // heuristic flag if AES encryption detected
// 7z IDs we care about
private const K_HEADER = 0x01;
private const K_ARCHIVE_PROPERTIES = 0x02; // skipped
private const K_ADDITIONAL_STREAMS_INFO = 0x03; // skipped
private const K_MAIN_STREAMS_INFO = 0x04; // skipped
private const K_FILES_INFO = 0x05;
private const K_END = 0x00;
private const K_ENCODED_HEADER = 0x17; // unsupported here
private const K_NAME = 0x11;
public function __construct(string $data)
{
$this->data = $data;
$this->len = strlen($data);
}
/**
* Public accessor: returns recovered filenames (UTF-8) or empty array.
*/
public function getFileNames(): array
{
if (! $this->parsed) {
$this->parse();
}
return $this->names;
}
/**
* Public accessor: returns true if AES encryption is detected (heuristic).
*/
public function isEncrypted(): bool
{
if (! $this->parsed) {
$this->parse();
}
return $this->encrypted;
}
public function hasEncodedHeader(): bool
{
if (! $this->parsed) {
$this->parse();
}
return $this->encodedHeader;
}
private function parse(): void
{
$this->parsed = true;
if ($this->len < 32) { // need at least fixed header
return;
}
// Signature check
if (strncmp($this->data, "\x37\x7A\xBC\xAF\x27\x1C", 6) !== 0) {
return;
}
$nextHeaderOffset = $this->readUInt64LE(12);
$nextHeaderSize = $this->readUInt64LE(20);
// Bounds sanity
if ($nextHeaderSize <= 0 || $nextHeaderSize > 4 * 1024 * 1024) { // cap 4MB header
return;
}
$nextHeaderStart = 32 + $nextHeaderOffset;
$nextHeaderEnd = $nextHeaderStart + $nextHeaderSize;
if ($nextHeaderEnd > $this->len) { // incomplete buffer
return;
}
$cursor = $nextHeaderStart;
// First byte may be kEncodedHeader (unsupported) or kHeader
$id = ord($this->data[$cursor]);
if ($id === self::K_ENCODED_HEADER) {
$this->encodedHeader = true; // caller can try external 7z listing fallback
// Heuristic: scan a limited window after this byte for AES method ID (06 F1 07 01) indicating encryption.
$scan = substr($this->data, $cursor, min(512, $this->len - $cursor));
if (strpos($scan, "\x06\xF1\x07\x01") !== false) {
$this->encrypted = true;
}
return; // we don't decode here
}
if ($id !== self::K_HEADER) {
return; // unexpected structure
}
$cursor++;
// Loop until K_END looking for K_FILES_INFO (0x05)
while ($cursor < $nextHeaderEnd) {
$id = ord($this->data[$cursor]);
$cursor++;
if ($id === self::K_END) {
break; // done
}
if ($id === self::K_FILES_INFO) {
$cursor = $this->parseFilesInfo($cursor, $nextHeaderEnd);
break; // stop after names
} else {
// Skip blocks we don't parse by walking their internal structure heuristically.
// For archive/main streams info we skip until their terminating K_END.
if (in_array($id, [self::K_ARCHIVE_PROPERTIES, self::K_ADDITIONAL_STREAMS_INFO, self::K_MAIN_STREAMS_INFO], true)) {
$cursor = $this->skipUntilEnd($cursor, $nextHeaderEnd);
if ($cursor === -1) {
break;
}
} else {
// Unknown ID bail out
break;
}
}
}
}
private function parseFilesInfo(int $cursor, int $limit): int
{
// Number of files (VInt)
$numFiles = $this->readVIntAt($cursor, $value, $newCursor, $limit) ? $value : null;
if ($numFiles === null || $numFiles < 0 || $numFiles > 10000) { // sanity cap
return $limit; // abort
}
$cursor = $newCursor;
// Property loop until K_END
$names = [];
while ($cursor < $limit) {
$propId = ord($this->data[$cursor]);
$cursor++;
if ($propId === self::K_END) {
break;
}
// Size of property data (VInt)
if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) {
break;
}
if ($propSize < 0 || $propSize > ($limit - $cursor)) {
break;
}
if ($propId === self::K_NAME) {
if ($propSize < 1) {
break;
}
$external = ord($this->data[$cursor]);
if ($external !== 0) { // External data not supported
break;
}
$nameBytes = $propSize - 1;
$cursor++;
if ($nameBytes <= 0) {
break;
}
$blob = substr($this->data, $cursor, $nameBytes);
// Ensure even length for UTF-16LE. Truncate last byte if odd.
if (($nameBytes & 1) === 1) {
$blob = substr($blob, 0, -1);
}
// Split on UTF-16LE null terminators (00 00)
$segments = preg_split('/\x00\x00/', $blob);
foreach ($segments as $seg) {
if ($seg === '') {
continue;
}
$utf8 = @iconv('UTF-16LE', 'UTF-8//IGNORE', $seg); // may return false
if ($utf8 === false) {
continue;
}
$utf8 = trim($utf8);
if ($utf8 === '') {
continue;
}
// Basic filtering exclude paths with directory separators beyond simple relative path
$utf8Clean = str_replace(['\\'], '/', $utf8);
// Remove leading './'
$utf8Clean = preg_replace('#^\./#', '', $utf8Clean);
if ($utf8Clean === '' || substr_count($utf8Clean, '/') > 8) { // excessive depth -> skip
continue;
}
$names[] = $utf8Clean;
if (count($names) >= $numFiles) {
break;
}
}
// Done reading this property
$cursor += $nameBytes;
// We collected names; we can stop early.
$this->names = array_values(array_unique($names));
return $limit;
} else {
// Skip property we don't care about
$cursor += $propSize;
}
}
// Assign if we gathered any
if ($names) {
$this->names = array_values(array_unique($names));
}
return $limit;
}
private function skipUntilEnd(int $cursor, int $limit): int
{
while ($cursor < $limit) {
$id = ord($this->data[$cursor]);
$cursor++;
if ($id === self::K_END) {
return $cursor;
}
// Property-like: read size then skip
if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) {
return -1;
}
if ($propSize < 0 || $propSize > ($limit - $cursor)) {
return -1;
}
$cursor += $propSize;
}
return -1;
}
/**
* Reads a 7z variable-length integer at offset, returns value via reference.
*/
private function readVIntAt(int $offset, ?int &$value, ?int &$newOffset, int $limit): bool
{
$value = 0;
$shift = 0;
$pos = $offset;
while ($pos < $limit && $shift <= 63) {
$b = ord($this->data[$pos]);
$pos++;
$value |= ($b & 0x7F) << $shift;
if (($b & 0x80) === 0) {
$newOffset = $pos;
return true;
}
$shift += 7;
}
return false;
}
private function readUInt64LE(int $offset): int
{
if ($offset + 8 > $this->len) {
return 0;
}
$v = 0;
for ($i = 0; $i < 8; $i++) {
$v |= ord($this->data[$offset + $i]) << ($i * 8);
}
// Constrain to PHP int (on 64-bit fine; on 32-bit may overflow but those environments uncommon here)
return $v & 0xFFFFFFFFFFFFFFFF;
}
}
+1 -1
View File
@@ -30,7 +30,7 @@
"conditions": [] "conditions": []
}, },
{ {
"action": "vendor/bin/php-cs-fixer fix", "action": "vendor/bin/pint",
"options": [], "options": [],
"conditions": [] "conditions": []
} }
+1 -1
View File
@@ -29,6 +29,7 @@
"ext-iconv": "*", "ext-iconv": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-mysqlnd": "*", "ext-mysqlnd": "*",
"ext-openssl": "*", "ext-openssl": "*",
@@ -200,7 +201,6 @@
"post-create-project-cmd": [ "post-create-project-cmd": [
"@php artisan key:generate --ansi" "@php artisan key:generate --ansi"
], ],
"check-style": "php-cs-fixer fix --dry-run --diff",
"fix-style": "pint" "fix-style": "pint"
} }
} }
Generated
+42 -42
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "10251ecba0052663a759a5be58b311d9", "content-hash": "ee41533387c7cf6236214d5ba2035c28",
"packages": [ "packages": [
{ {
"name": "aharen/omdbapi", "name": "aharen/omdbapi",
@@ -1022,23 +1022,23 @@
}, },
{ {
"name": "dariusiii/tmdb-laravel", "name": "dariusiii/tmdb-laravel",
"version": "12.0.3", "version": "12.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/DariusIII/tmdb-laravel.git", "url": "https://github.com/DariusIII/tmdb-laravel.git",
"reference": "3c0fa42a0bd569b2a550cbc7631eab56c889f551" "reference": "c0b9ddf6f7094392cae174eb05fd200e3dbfea91"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/DariusIII/tmdb-laravel/zipball/3c0fa42a0bd569b2a550cbc7631eab56c889f551", "url": "https://api.github.com/repos/DariusIII/tmdb-laravel/zipball/c0b9ddf6f7094392cae174eb05fd200e3dbfea91",
"reference": "3c0fa42a0bd569b2a550cbc7631eab56c889f551", "reference": "c0b9ddf6f7094392cae174eb05fd200e3dbfea91",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
"nyholm/psr7": "^1.8", "nyholm/psr7": "^1.8",
"php": ">=8.0", "php": ">=8.3",
"php-tmdb/api": "^5.0" "php-tmdb/api": "^5.0"
}, },
"require-dev": { "require-dev": {
@@ -1092,9 +1092,9 @@
"wrapper" "wrapper"
], ],
"support": { "support": {
"source": "https://github.com/DariusIII/tmdb-laravel/tree/12.0.3" "source": "https://github.com/DariusIII/tmdb-laravel/tree/12.1"
}, },
"time": "2025-03-26T11:19:20+00:00" "time": "2025-09-02T12:45:28+00:00"
}, },
{ {
"name": "dariusiii/tv-maze-php-api", "name": "dariusiii/tv-maze-php-api",
@@ -15781,16 +15781,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.86.0", "version": "v3.87.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36" "reference": "50a13c4c5f25d2c6894e30e92c051474cf0e115a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50a13c4c5f25d2c6894e30e92c051474cf0e115a",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36", "reference": "50a13c4c5f25d2c6894e30e92c051474cf0e115a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -15801,39 +15801,38 @@
"ext-hash": "*", "ext-hash": "*",
"ext-json": "*", "ext-json": "*",
"ext-tokenizer": "*", "ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.2", "fidry/cpu-core-counter": "^1.3",
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6", "react/child-process": "^0.6.6",
"react/event-loop": "^1.5", "react/event-loop": "^1.5",
"react/promise": "^3.2", "react/promise": "^3.3",
"react/socket": "^1.16", "react/socket": "^1.16",
"react/stream": "^1.4", "react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
"symfony/console": "^5.4.47 || ^6.4.13 || ^7.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/polyfill-mbstring": "^1.32", "symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.32", "symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.32", "symfony/polyfill-php81": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
}, },
"require-dev": { "require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.6", "facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14", "infection/infection": "^0.29.14",
"justinrainbow/json-schema": "^5.3 || ^6.4", "justinrainbow/json-schema": "^6.4",
"keradus/cli-executor": "^2.2", "keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12", "mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.8", "php-coveralls/php-coveralls": "^2.8",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
"symfony/polyfill-php84": "^1.32", "symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
}, },
"suggest": { "suggest": {
"ext-dom": "For handling output formats in XML", "ext-dom": "For handling output formats in XML",
@@ -15874,7 +15873,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.86.0" "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v97773{PHP_CS_FIXER_VERSION}"
}, },
"funding": [ "funding": [
{ {
@@ -15882,7 +15881,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-08-13T22:36:21+00:00" "time": "2025-09-02T10:58:35+00:00"
}, },
{ {
"name": "hamcrest/hamcrest-php", "name": "hamcrest/hamcrest-php",
@@ -16969,34 +16968,34 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "12.3.4", "version": "12.3.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "7ad0e9bdc72b147600badccd694a2e57ffc9297a" "reference": "da2cdaff87220fa641e7652364281b736e4347e0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7ad0e9bdc72b147600badccd694a2e57ffc9297a", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/da2cdaff87220fa641e7652364281b736e4347e0",
"reference": "7ad0e9bdc72b147600badccd694a2e57ffc9297a", "reference": "da2cdaff87220fa641e7652364281b736e4347e0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^5.4.0", "nikic/php-parser": "^5.6.1",
"php": ">=8.3", "php": ">=8.3",
"phpunit/php-file-iterator": "^6.0", "phpunit/php-file-iterator": "^6.0",
"phpunit/php-text-template": "^5.0", "phpunit/php-text-template": "^5.0",
"sebastian/complexity": "^5.0", "sebastian/complexity": "^5.0",
"sebastian/environment": "^8.0", "sebastian/environment": "^8.0.3",
"sebastian/lines-of-code": "^4.0", "sebastian/lines-of-code": "^4.0",
"sebastian/version": "^6.0", "sebastian/version": "^6.0",
"theseer/tokenizer": "^1.2.3" "theseer/tokenizer": "^1.2.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^12.1" "phpunit/phpunit": "^12.3.7"
}, },
"suggest": { "suggest": {
"ext-pcov": "PHP extension that provides line coverage", "ext-pcov": "PHP extension that provides line coverage",
@@ -17034,7 +17033,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.4" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.6"
}, },
"funding": [ "funding": [
{ {
@@ -17054,7 +17053,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-29T11:32:44+00:00" "time": "2025-09-02T05:23:14+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@@ -19316,6 +19315,7 @@
"ext-iconv": "*", "ext-iconv": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-mysqlnd": "*", "ext-mysqlnd": "*",
"ext-openssl": "*", "ext-openssl": "*",
+1
View File
@@ -8,6 +8,7 @@ return [
'delete_passworded_releases' => env('DELETE_PASSWORDED_RELEASES', false), 'delete_passworded_releases' => env('DELETE_PASSWORDED_RELEASES', false),
'delete_possible_passworded_releases' => env('DELETE_POSSIBLE_PASSWORDED_RELEASES', false), 'delete_possible_passworded_releases' => env('DELETE_POSSIBLE_PASSWORDED_RELEASES', false),
'extract_using_rarinfo' => env('EXTRACT_USING_RARINFO', false), 'extract_using_rarinfo' => env('EXTRACT_USING_RARINFO', false),
'fetch_last_file' => env('FETCH_LAST_FILE', true),
'path_to_nzbs' => env('PATH_TO_NZBS', storage_path('nzb')), 'path_to_nzbs' => env('PATH_TO_NZBS', storage_path('nzb')),
'private_profiles' => env('PRIVATE_PROFILES', true), 'private_profiles' => env('PRIVATE_PROFILES', true),
'store_user_ip' => env('STORE_USER_IP', false), 'store_user_ip' => env('STORE_USER_IP', false),