From 1e314ae818e4d98fa91937e3a4b0468da19f6089 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Mon, 23 Feb 2026 14:45:30 +0100 Subject: [PATCH] Drop support for 7z archives --- .env.example | 1 - Dockerfile | 15 +- .../AdditionalProcessingOrchestrator.php | 6 +- .../ArchiveExtractionService.php | 528 +---- .../Config/ProcessingConfiguration.php | 2 - .../ConsoleOutputService.php | 4 +- .../AdditionalProcessing/NzbContentParser.php | 2 +- .../ReleaseFileManager.php | 2 +- .../Archives/SevenZipPartialParser.php | 1787 ----------------- config/nntmux_settings.php | 1 - docker/8.4/Dockerfile | 15 +- docker/8.5/Dockerfile | 15 +- 12 files changed, 11 insertions(+), 2367 deletions(-) delete mode 100644 app/Services/Archives/SevenZipPartialParser.php diff --git a/.env.example b/.env.example index 43d6982ef..040d328e2 100644 --- a/.env.example +++ b/.env.example @@ -184,7 +184,6 @@ TEMP_UNZIP_PATH='/var/www/nntmux/resources/tmp/unzip/' # Binary paths - adjust these to match your system UNRAR_PATH=/usr/bin/unrar UNZIP_PATH=/usr/bin/unzip -S7ZIP_PATH=/usr/bin/7z FFMPEG_PATH=/usr/bin/ffmpeg LAME_PATH=/usr/bin/lame MEDIAINFO_PATH=/usr/bin/mediainfo diff --git a/Dockerfile b/Dockerfile index 41f2f903d..13bb62ea6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ FROM dunglas/frankenphp:1-php8.3 LABEL maintainer="PyRowMan" ENV SERVER_NAME=:${APP_PORT:-80} ARG MYSQL_CLIENT="mariadb-client" -ARG SEVENZIP_VERSION=2407 WORKDIR /app @@ -14,7 +13,7 @@ COPY --from=composer-base --link /usr/bin/composer /usr/bin/composer RUN apt update \ && apt install -y --no-install-recommends \ - unrar-free 7zip lame libcap2-bin python3 gettext-base \ + unrar-free lame libcap2-bin python3 gettext-base \ curl zip unzip git nano bash-completion sudo wget tmux time fonts-powerline \ gnupg libpng-dev dnsutils jq htop iputils-ping net-tools ffmpeg \ jpegoptim webp optipng pngquant libavif-bin watch iproute2 nmon \ @@ -36,18 +35,6 @@ RUN docker-php-ext-install \ && docker-php-ext-enable redis \ && apt clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Determine ARCH and download and extract the appropriate version of 7-Zip -RUN ARCH="$(dpkg --print-architecture)" && \ - if [ "$ARCH" = "amd64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-x64.tar.xz"; \ - fi && \ - if [ "$ARCH" = "arm64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-arm64.tar.xz"; \ - fi && \ - wget "$SZIP_URL" -O /tmp/7z.tar.xz && \ - tar -xf /tmp/7z.tar.xz -C /tmp/ && \ - mv /tmp/7zz /usr/bin/7zz && \ - rm -f /tmp/7z.tar.xz && rm -f /tmp/7zzs RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY ./docker/8.3/php.ini "$PHP_INI_DIR/conf.d/custom-conf.ini" diff --git a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php index ac9fc1253..59fe6b1fc 100644 --- a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php +++ b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php @@ -500,7 +500,7 @@ class AdditionalProcessingOrchestrator $title = $nzbFile['title'] ?? ''; if (! preg_match( - '/(\\.(part\\d+|[rz]\\d+|rar|0+|0*10?|zipr\\d{2,3}|zipx?|7z(?:\\.\\d{3})?|(?:tar\\.)?(?:gz|bz2|xz))(\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$)/i', + '/(\\.(part\\d+|[rz]\\d+|rar|0+|0*10?|zipr\\d{2,3}|zipx?)(\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$)/i', $title )) { continue; @@ -737,7 +737,7 @@ class AdditionalProcessingOrchestrator $title = $nzbFile['title'] ?? ''; if (! preg_match( - '/(\\.(part0*1|rar|zip|7z))(\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$/i', + '/(\\.(part0*1|rar|zip))(\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$/i', $title )) { continue; @@ -816,7 +816,7 @@ class AdditionalProcessingOrchestrator } $foundCompressed = false; - $pattern = '/.*\.([rz]\d{2,}|rar|zipx?|0{0,2}1|7z(?:\.\d{3})?|(?:tar\.)?(?:gz|bz2|xz))($|[^a-z0-9])/i'; + $pattern = '/.*\.([rz]\d{2,}|rar|zipx?|0{0,2}1)($|[^a-z0-9])/i'; try { $files = $this->tempWorkspace->listFiles($context->tmpPath, $pattern); diff --git a/app/Services/AdditionalProcessing/ArchiveExtractionService.php b/app/Services/AdditionalProcessing/ArchiveExtractionService.php index 475db5e95..b6a672987 100644 --- a/app/Services/AdditionalProcessing/ArchiveExtractionService.php +++ b/app/Services/AdditionalProcessing/ArchiveExtractionService.php @@ -11,7 +11,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; /** - * Service for extracting and processing archive files (RAR, ZIP, 7z, gzip, bzip2, xz). + * Service for extracting and processing archive files (RAR, ZIP). * Handles password detection, file listing, and content extraction. */ class ArchiveExtractionService @@ -51,25 +51,6 @@ class ArchiveExtractionService $context->compressedFilesChecked++; - // Detect archive type early - $archiveType = $this->detectArchiveType($compressedData); - - // Handle 7z, gzip, bzip2, xz with external 7zip binary - if (in_array($archiveType, ['7z', 'gzip', 'bzip2', 'xz'], true)) { - if ($archiveType === '7z') { - $sevenZipResult = $this->processSevenZipArchive($compressedData, $context, $tmpPath); - if ($sevenZipResult['success'] || $sevenZipResult['hasPassword']) { - return $sevenZipResult; - } - } - - if ($this->config->sevenZipPath) { - $extractResult = $this->extractViaSevenZip($compressedData, $archiveType, $tmpPath); - if ($extractResult['success']) { // @phpstan-ignore offsetAccess.notFound - return $extractResult; - } - } - } // Try ArchiveInfo for RAR/ZIP if (! $this->archiveInfo->setData($compressedData, true)) { @@ -145,425 +126,6 @@ class ArchiveExtractionService ]; } - /** - * Detect the archive type from binary signature. - */ - public function detectArchiveType(string $data): ?string - { - $head6 = substr($data, 0, 6); - $head4 = substr($data, 0, 4); - - // 7z signature - if ($head6 === "\x37\x7A\xBC\xAF\x27\x1C" && $this->isLikely7z($data)) { - return '7z'; - } - // GZIP - if (strncmp($head4, "\x1F\x8B\x08", 3) === 0) { - return 'gzip'; - } - // BZip2 - if (strncmp($head4, 'BZh', 3) === 0) { - return 'bzip2'; - } - // XZ - if ($head6 === "\xFD7zXZ\x00") { - return 'xz'; - } - // PDF (skip) - if ($head4 === '%PDF') { - return 'pdf'; - } - - return null; - } - - /** - * Heuristic validation for 7z signature. - */ - private function isLikely7z(string $data): bool - { - if (strlen($data) < 32) { - return false; - } - $verMajor = ord($data[6]); - $verMinor = ord($data[7]); - if ($verMajor !== 0x00 || $verMinor < 0x02 || $verMinor > 0x09) { - return false; - } - $crc = substr($data, 8, 4); - if ($crc === "\x00\x00\x00\x00" || $crc === "\xFF\xFF\xFF\xFF") { - return false; - } - - return true; - } - - /** - * Process a 7z archive using external binary and internal header parsing. - * - * @return array - */ - private function processSevenZipArchive( - string $compressedData, - ReleaseProcessingContext $context, - string $tmpPath - ): array { - $result = [ - 'success' => false, - 'files' => [], - 'hasPassword' => false, - 'passwordStatus' => ReleaseBrowseService::PASSWD_NONE, - ]; - - if (! $this->config->sevenZipPath) { - return $result; - } - - // Try listing with external 7z binary - $listed = $this->listSevenZipEntries($compressedData, $tmpPath); - if (! empty($listed)) { - if (! empty($listed[0]['__any_encrypted__'])) { - return [ - 'success' => false, - 'files' => [], - 'hasPassword' => true, - 'passwordStatus' => ReleaseBrowseService::PASSWD_RAR, - ]; - } - - $files = $this->filterSevenZipFiles($listed); - if (! empty($files)) { - return [ - 'success' => true, - 'files' => $files, - 'hasPassword' => false, - 'passwordStatus' => ReleaseBrowseService::PASSWD_NONE, - 'archiveMarker' => '7z', - ]; - } - } - - // Fallback: scan for filenames in raw data - $scannedNames = $this->scanSevenZipFilenames($compressedData); - if (! empty($scannedNames)) { - $files = array_map(fn ($name) => [ - 'name' => $name, - 'size' => 0, - 'date' => time(), - 'pass' => 0, - 'crc32' => '', - 'source' => '7z-scan', - ], $scannedNames); - - return [ - 'success' => true, - 'files' => $files, - 'hasPassword' => false, - 'passwordStatus' => ReleaseBrowseService::PASSWD_NONE, - 'archiveMarker' => '7z', - ]; - } - - return $result; - } - - /** - * List entries of a 7z archive using external 7z binary. - * - * @return array - */ - public function listSevenZipEntries(string $compressedData, string $tmpPath): array - { - if (! $this->config->sevenZipPath) { - return []; - } - - try { - $tmpFile = $tmpPath.uniqid('7zlist_', true).'.7z'; - if (File::put($tmpFile, $compressedData) === false) { - return []; - } - - $cmd = [$this->config->sevenZipPath, 'l', '-slt', '-ba', '-bd', $tmpFile]; - $exitCode = 0; - $stdout = null; - $stderr = null; - $ok = $this->execCommand($cmd, $exitCode, $stdout, $stderr); // @phpstan-ignore argument.type - - if (! $ok || $exitCode !== 0 || empty($stdout)) { - // Try plain listing fallback - $plainResult = $this->listSevenZipPlain($tmpFile); - File::delete($tmpFile); - - return $plainResult; - } - - File::delete($tmpFile); - - return $this->parseSevenZipStructuredOutput($stdout); - } catch (\Throwable $e) { - if ($this->config->debugMode) { - Log::debug('Exception listing 7z: '.$e->getMessage()); - } - - return []; - } - } - - /** - * Plain 7z listing fallback. - * - * @return list> - */ - private function listSevenZipPlain(string $tmpFile): array - { - $cmd = [$this->config->sevenZipPath, 'l', '-ba', '-bd', $tmpFile]; - $exitCode = 0; - $stdout = null; - $stderr = null; - $ok = $this->execCommand($cmd, $exitCode, $stdout, $stderr); // @phpstan-ignore argument.type - - if (! $ok || $exitCode !== 0 || empty($stdout)) { - return []; - } - - $files = []; - $lines = preg_split('/\r?\n/', trim($stdout)); - foreach ($lines as $line) { - $line = trim($line); - if ($line === '' || str_starts_with($line, '-----') - || str_contains($line, ' Date ') - || str_starts_with($line, 'Scanning ') - ) { - continue; - } - - $name = null; - if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+\S+\s+\d+\s+\d+\s+(\S.*)$/', $line, $m)) { - $name = $m[1]; - } elseif (preg_match('/([A-Za-z0-9_#@()\[\]\-+&., ]+\.[A-Za-z0-9]{2,8})$/', $line, $m2)) { - $name = trim($m2[1]); - } - - if ($name && strlen($name) <= 300) { - $files[] = ['name' => trim($name), 'size' => 0, 'encrypted' => false]; - if (count($files) >= 200) { - break; - } - } - } - - return $files; - } - - /** - * Parse structured 7z output. - * - * @return list> - */ - private function parseSevenZipStructuredOutput(string $output): array - { - $blocks = preg_split('/\n\n+/u', trim($output)); - $files = []; - $anyEncrypted = false; - - foreach ($blocks as $block) { - $lines = preg_split('/\r?\n/', trim($block)); - $row = []; - foreach ($lines as $line) { - $kv = explode(' = ', $line, 2); - if (count($kv) === 2) { - $row[$kv[0]] = $kv[1]; - } - } - - if (empty($row['Path'])) { - continue; - } - - $attr = $row['Attributes'] ?? ''; - if (str_contains($attr, 'D')) { - continue; // directory - } - - $encrypted = ($row['Encrypted'] ?? '') === '+'; - if ($encrypted) { - $anyEncrypted = true; - } - - $size = isset($row['Size']) && ctype_digit($row['Size']) ? (int) $row['Size'] : 0; - $files[] = ['name' => $row['Path'], 'size' => $size, 'encrypted' => $encrypted]; - - if (count($files) >= 200) { - break; - } - } - - if ($anyEncrypted && isset($files[0])) { - $files[0]['__any_encrypted__'] = true; - } - - return $files; - } - - /** - * Filter 7z files using extension whitelist. - * - * @param array $files - * @return list - */ - private function filterSevenZipFiles(array $files): array - { - $allowedExtensions = $this->getAllowedExtensions(); - $filtered = []; - - foreach ($files as $entry) { - if (! empty($entry['__any_encrypted__'])) { - continue; - } - - $name = $entry['name'] ?? ''; - if ($name === '' || strlen($name) > 300) { - continue; - } - - $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - if (! in_array($ext, $allowedExtensions, true)) { - continue; - } - - $base = pathinfo($name, PATHINFO_FILENAME); - $letterCount = preg_match_all('/[a-z]/i', $base); - if ($letterCount <= 5) { - continue; - } - - $filtered[] = [ - 'name' => $name, - 'size' => $entry['size'] ?? 0, - 'date' => time(), - 'pass' => 0, - 'crc32' => '', - 'source' => '7z-list', - ]; - - if (count($filtered) >= 50) { - break; - } - } - - return $filtered; - } - - /** - * Scan for filenames in 7z raw data. - * - * @return list, pass: 0, crc32: '', source: '7z-list'}> - */ - private function scanSevenZipFilenames(string $data): array - { - $slice = substr($data, 0, 8 * 1024 * 1024); - $converted = preg_replace('/([\x20-\x7E])\x00/', '$1', $slice) ?? $slice; - $converted = str_replace("\x00", ' ', $converted); - - $exts = '7z|rar|zip|gz|bz2|xz|tar|tgz|mp4|mkv|avi|mpg|mpeg|mov|ts|wmv|flv|m4v|mp3|flac|ogg|wav|aac|aiff|ape|mka|nfo|txt|diz|pdf|epub|mobi|jpg|jpeg|png|gif|sfv|par2|exe|dll|srt|sub|idx|iso|bin|cue|mds|mdf'; - $regex = '~(?:[A-Za-z0-9 _.+&@#,()!-]{0,120}[\\/])?([A-Za-z0-9 _.+&@#,()!-]{2,160}\.(?:'.$exts.'))~i'; - preg_match_all($regex, $converted, $matches, PREG_SET_ORDER); - - if (empty($matches)) { - return []; - } - - $names = []; - foreach ($matches as $match) { - $candidate = preg_replace('/ {2,}/', ' ', $match[1]); - if ($candidate === '' || strlen($candidate) < 5 || substr_count($candidate, '.') > 10) { - continue; - } - $candidate = trim($candidate, " .-\t\n\r"); - $lower = strtolower($candidate); - if (! isset($names[$lower])) { - $names[$lower] = $candidate; - if (count($names) >= 80) { - break; - } - } - } - - return array_values($names); - } - - /** - * Extract using 7zip binary. - * - * @return list - */ - public function extractViaSevenZip(string $compressedData, string $type, string $tmpPath): array - { - $result = [ - 'success' => false, - 'files' => [], - 'hasPassword' => false, - 'passwordStatus' => ReleaseBrowseService::PASSWD_NONE, - ]; - - if ($this->config->extractUsingRarInfo || ! $this->config->sevenZipPath) { - return $result; - } - - try { - $extMap = ['7z' => '7z', 'gzip' => 'gz', 'bzip2' => 'bz2', 'xz' => 'xz']; - $markerMap = ['7z' => '7z', 'gzip' => 'g', 'bzip2' => 'b', 'xz' => 'x']; - $ext = $extMap[$type] ?? 'dat'; - $marker = $markerMap[$type] ?? $type; - - $extractDir = $tmpPath.'un7z/'.uniqid('', true).'/'; - if (! File::isDirectory($extractDir)) { - File::makeDirectory($extractDir, 0777, true, true); - } - - $fileName = $tmpPath.uniqid('', true).'.'.$ext; - File::put($fileName, $compressedData); - - $cmd = [$this->config->sevenZipPath, 'e', '-y', '-bd', '-o'.$extractDir, $fileName]; - $exitCode = 0; - $stdout = null; - $stderr = null; - $this->execCommand($cmd, $exitCode, $stdout, $stderr); // @phpstan-ignore argument.type - - $files = []; - if (File::isDirectory($extractDir)) { - foreach (File::allFiles($extractDir) as $f) { - $files[] = [ - 'name' => $f->getFilename(), - 'size' => $f->getSize(), - 'date' => time(), - 'pass' => 0, - 'crc32' => '', - 'source' => $type, - ]; - } - } - - File::delete($fileName); - - if (! empty($files)) { - return [ // @phpstan-ignore return.type - 'success' => true, - 'files' => $this->filterExtractedFiles($files), // @phpstan-ignore argument.type - 'hasPassword' => false, - 'passwordStatus' => ReleaseBrowseService::PASSWD_NONE, - 'archiveMarker' => $marker, - ]; - } - } catch (\Throwable $e) { - if ($this->config->debugMode) { - Log::warning(strtoupper($type).' extraction exception: '.$e->getMessage()); - } - } - - return $result; - } /** * Filter extracted files by allowed extensions. @@ -832,11 +394,6 @@ class ArchiveExtractionService } // Fallback: use external tools to extract to temp directory - $archiveType = $this->detectArchiveType($compressedData); - - if ($archiveType === '7z' && $this->config->sevenZipPath) { - return $this->extractFileVia7zip($compressedData, $filename, $tmpPath); - } // Try using unrar for RAR files if ($this->config->unrarPath) { @@ -857,58 +414,6 @@ class ArchiveExtractionService return null; } - /** - * Extract a specific file using 7zip. - */ - private function extractFileVia7zip(string $compressedData, string $filename, string $tmpPath): ?string - { - try { - $extractDir = $tmpPath.'extract_'.uniqid('', true).'/'; - if (! File::isDirectory($extractDir)) { - File::makeDirectory($extractDir, 0777, true, true); - } - - $archiveFile = $tmpPath.'archive_'.uniqid('', true).'.7z'; - File::put($archiveFile, $compressedData); - - // Extract specific file - $cmd = [$this->config->sevenZipPath, 'e', '-y', '-bd', '-o'.$extractDir, $archiveFile, $filename]; - $exitCode = 0; - $stdout = null; - $stderr = null; - $this->execCommand($cmd, $exitCode, $stdout, $stderr); // @phpstan-ignore argument.type - - File::delete($archiveFile); - - // Look for extracted file - $extractedPath = $extractDir.basename($filename); - if (File::isFile($extractedPath)) { - $content = File::get($extractedPath); - File::deleteDirectory($extractDir); - - return $content; - } - - // Try to find it with glob (in case path differs) - $files = File::allFiles($extractDir); - foreach ($files as $file) { - if (strtolower($file->getFilename()) === strtolower(basename($filename))) { - $content = File::get($file->getPathname()); - File::deleteDirectory($extractDir); - - return $content; - } - } - - File::deleteDirectory($extractDir); - } catch (\Throwable $e) { - if ($this->config->debugMode) { - Log::debug('7zip extraction failed: '.$e->getMessage()); - } - } - - return null; - } /** * Extract a specific file using unrar. @@ -1009,35 +514,4 @@ class ArchiveExtractionService return null; } - /** - * Execute a command with output capture. - * - * @param-out int $exitCode - * @param-out string|null $stdout - * @param-out string|null $stderr - * - * @param array $cmd - */ - private function execCommand(array $cmd, ?int &$exitCode, ?string &$stdout, ?string &$stderr): bool - { - $descriptorSpec = [ - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - - $process = @proc_open($cmd, $descriptorSpec, $pipes, null, null, ['bypass_shell' => true]); // @phpstan-ignore argument.type - if (! is_resource($process)) { - $exitCode = -1; - - return false; - } - - $stdout = stream_get_contents($pipes[1]) ?: null; - fclose($pipes[1]); - $stderr = stream_get_contents($pipes[2]) ?: null; - fclose($pipes[2]); - $exitCode = proc_close($process); - - return $exitCode === 0; - } } diff --git a/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php b/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php index 7bff63758..0cc965199 100644 --- a/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php +++ b/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php @@ -24,7 +24,6 @@ final readonly class ProcessingConfiguration public string|false $unzipPath; - public string|false $sevenZipPath; public string|false $timeoutPath; @@ -106,7 +105,6 @@ final readonly class ProcessingConfiguration $this->fetchLastFiles = (bool) config('nntmux_settings.fetch_last_file'); $this->unrarPath = config('nntmux_settings.unrar_path') ?: false; $this->unzipPath = config('nntmux_settings.unzip_path') ?: false; - $this->sevenZipPath = config('nntmux_settings.7zip_path') ?: false; $this->timeoutPath = config('nntmux_settings.timeout_path') ?: false; $this->timeoutSeconds = (int) Settings::settingValue('timeoutseconds'); $this->queryLimit = (int) (Settings::settingValue('maxaddprocessed') ?: 25); diff --git a/app/Services/AdditionalProcessing/ConsoleOutputService.php b/app/Services/AdditionalProcessing/ConsoleOutputService.php index dab7a379f..4a37a1f2e 100644 --- a/app/Services/AdditionalProcessing/ConsoleOutputService.php +++ b/app/Services/AdditionalProcessing/ConsoleOutputService.php @@ -38,11 +38,11 @@ class ConsoleOutputService PHP_EOL. 'Failures: fC#=Compressed(part #), fS=Sample, fM=Media(video), fA=Audio, fJ=JPEG, G=Missing group, T=Timeout skip'. PHP_EOL. - 'Processing: r=RAR, z=ZIP, 7z=7zip (names/entries), g=GZIP, b=BZIP2, x=XZ, (vRAW)=Inline video detected'. + 'Processing: r=RAR, z=ZIP, (vRAW)=Inline video detected'. PHP_EOL. 'Added: s=Sample image, j=JPEG image, A=Audio sample, a=Audio MediaInfo, v=Video sample'. PHP_EOL. - 'Added: m=Video MediaInfo, n=NFO, ^=Inner file details (RAR/ZIP/7z/etc)'. + 'Added: m=Video MediaInfo, n=NFO, ^=Inner file details (RAR/ZIP)'. '', 'header' ); diff --git a/app/Services/AdditionalProcessing/NzbContentParser.php b/app/Services/AdditionalProcessing/NzbContentParser.php index 92b9d7905..e66321758 100644 --- a/app/Services/AdditionalProcessing/NzbContentParser.php +++ b/app/Services/AdditionalProcessing/NzbContentParser.php @@ -190,7 +190,7 @@ class NzbContentParser // Compressed file detection if (! $result['hasCompressedFile'] && preg_match( - '/(\\.(part\\d+|[rz]\\d+|rar|0+|0*10?|zipr\\d{2,3}|zipx?|7z(?:\\.\\d{3})?|(?:tar\\.)?(?:gz|bz2|xz))("|\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$)/i', + '/(\\.(part\\d+|[rz]\\d+|rar|0+|0*10?|zipr\\d{2,3}|zipx?)("|\\s*\\.rar)*($|[ ")]|-])|"[a-f0-9]{32}\\.[1-9]\\d{1,2}".*\\(\\d+\\/\\d{2,}\\)$)/i', $title )) { $result['hasCompressedFile'] = true; diff --git a/app/Services/AdditionalProcessing/ReleaseFileManager.php b/app/Services/AdditionalProcessing/ReleaseFileManager.php index 05bcfc52e..e4d8a878a 100644 --- a/app/Services/AdditionalProcessing/ReleaseFileManager.php +++ b/app/Services/AdditionalProcessing/ReleaseFileManager.php @@ -78,7 +78,7 @@ class ReleaseFileManager // Skip support files if (preg_match( - '/(?:'.$supportFileRegex.'|part\d+|[rz]\d{1,3}|zipr\d{2,3}|\d{2,3}|zipx?|zip|rar|7z|gz|bz2|xz)(\s*\.rar)?$/i', + '/(?:'.$supportFileRegex.'|part\d+|[rz]\d{1,3}|zipr\d{2,3}|\d{2,3}|zipx?|zip|rar)(\s*\.rar)?$/i', $file['name'] )) { return false; diff --git a/app/Services/Archives/SevenZipPartialParser.php b/app/Services/Archives/SevenZipPartialParser.php deleted file mode 100644 index 7be7daed7..000000000 --- a/app/Services/Archives/SevenZipPartialParser.php +++ /dev/null @@ -1,1787 +0,0 @@ - - */ - private array $names = []; - - /** - * @var array - */ - private array $files = []; // Extended file info with metadata - - /** - * @var array - */ - private array $sizes = []; // Uncompressed sizes - - /** - * @var array - */ - private array $packedSizes = []; // Compressed sizes - - /** - * @var array - */ - private array $crcs = []; // CRC32 values - - /** - * @var array - */ - private array $attributes = []; // File attributes (directory, readonly, etc.) - - /** - * @var array - */ - private array $mtimes = []; // Modification times - - /** - * @var array - */ - private array $ctimes = []; // Creation times - - /** - * @var array - */ - private array $atimes = []; // Access times - - private bool $parsed = false; - - private bool $encodedHeader = false; // flag if we encountered kEncodedHeader - - private bool $encrypted = false; // heuristic flag if AES encryption detected - - private bool $headerEncrypted = false; // flag if header itself is encrypted - - private bool $solidArchive = false; // flag for solid archives - - private int $numFiles = 0; // Total number of files detected - - /** - * @var array - */ - private array $compressionMethods = []; // Detected compression methods - - private int $totalUnpackedSize = 0; // Total unpacked size - - private int $totalPackedSize = 0; // Total packed size - - private string $lastError = ''; // Last error message for debugging - - // 7z Property IDs - private const K_END = 0x00; - - private const K_HEADER = 0x01; - - private const K_ARCHIVE_PROPERTIES = 0x02; - - private const K_ADDITIONAL_STREAMS_INFO = 0x03; - - private const K_MAIN_STREAMS_INFO = 0x04; - - private const K_FILES_INFO = 0x05; - - private const K_PACK_INFO = 0x06; - - private const K_UNPACK_INFO = 0x07; - - private const K_SUBSTREAMS_INFO = 0x08; - - private const K_SIZE = 0x09; - - private const K_CRC = 0x0A; - - private const K_FOLDER = 0x0B; - - private const K_CODERS_UNPACK_SIZE = 0x0C; - - private const K_NUM_UNPACK_STREAM = 0x0D; - - private const K_EMPTY_STREAM = 0x0E; - - private const K_EMPTY_FILE = 0x0F; - - private const K_ANTI = 0x10; - - private const K_NAME = 0x11; - - private const K_CTIME = 0x12; - - private const K_ATIME = 0x13; - - private const K_MTIME = 0x14; - - private const K_WIN_ATTRIBUTES = 0x15; - - /** @phpstan-ignore classConstant.unused */ - private const K_COMMENT = 0x16; - - private const K_ENCODED_HEADER = 0x17; - - /** @phpstan-ignore classConstant.unused */ - private const K_START_POS = 0x18; - - /** @phpstan-ignore classConstant.unused */ - private const K_DUMMY = 0x19; - - // Compression method IDs - private const METHOD_COPY = "\x00"; - - private const METHOD_LZMA = "\x03\x01\x01"; - - private const METHOD_LZMA2 = "\x21"; - - private const METHOD_PPMD = "\x03\x04\x01"; - - /** @phpstan-ignore classConstant.unused */ - private const METHOD_BCJ = "\x03\x03\x01\x03"; - - /** @phpstan-ignore classConstant.unused */ - private const METHOD_BCJ2 = "\x03\x03\x01\x1B"; - - private const METHOD_DEFLATE = "\x04\x01\x08"; - - private const METHOD_BZIP2 = "\x04\x02\x02"; - - private const METHOD_AES = "\x06\xF1\x07\x01"; - - // Windows file attributes - /** @phpstan-ignore classConstant.unused */ - private const FILE_ATTRIBUTE_READONLY = 0x01; - - /** @phpstan-ignore classConstant.unused */ - private const FILE_ATTRIBUTE_HIDDEN = 0x02; - - /** @phpstan-ignore classConstant.unused */ - private const FILE_ATTRIBUTE_SYSTEM = 0x04; - - private const FILE_ATTRIBUTE_DIRECTORY = 0x10; - - /** @phpstan-ignore classConstant.unused */ - private const FILE_ATTRIBUTE_ARCHIVE = 0x20; - - public function __construct(string $data) - { - $this->data = $data; - $this->len = strlen($data); - } - - /** - * Public accessor: returns recovered filenames (UTF-8) or empty array. - * - * @return array - */ - public function getFileNames(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->names; - } - - /** - * Returns detailed file information with all available metadata. - * - * @return array - */ - public function getFiles(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->files; - } - - /** - * Returns uncompressed file sizes indexed by file index. - * - * @return array - */ - public function getSizes(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->sizes; - } - - /** - * Returns CRC32 values as hex strings indexed by file index. - * - * @return array - */ - public function getCRCs(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->crcs; - } - - /** - * Returns file attributes indexed by file index. - * - * @return array - */ - public function getAttributes(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->attributes; - } - - /** - * Returns modification times (Unix timestamps) indexed by file index. - * - * @return array - */ - public function getModificationTimes(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->mtimes; - } - - /** - * Returns detected compression methods used in the archive. - * - * @return array - */ - public function getCompressionMethods(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return array_unique($this->compressionMethods); - } - - /** - * Returns total number of files detected. - */ - public function getFileCount(): int - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->numFiles; - } - - /** - * Returns total unpacked (uncompressed) size of all files. - */ - public function getTotalUnpackedSize(): int - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->totalUnpackedSize; - } - - /** - * Returns total packed (compressed) size. - */ - public function getTotalPackedSize(): int - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->totalPackedSize; - } - - /** - * Public accessor: returns true if AES encryption is detected (heuristic). - */ - public function isEncrypted(): bool - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->encrypted; - } - - /** - * Returns true if the header itself is encrypted. - */ - public function isHeaderEncrypted(): bool - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->headerEncrypted; - } - - /** - * Returns true if this is a solid archive. - */ - public function isSolidArchive(): bool - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->solidArchive; - } - - public function hasEncodedHeader(): bool - { - if (! $this->parsed) { - $this->parse(); - } - - return $this->encodedHeader; - } - - /** - * Returns the last error message for debugging. - */ - public function getLastError(): string - { - return $this->lastError; - } - - /** - * Check if the data appears to be a valid 7z archive (has correct signature). - */ - public function isValid7zSignature(): bool - { - return $this->len >= 6 && strncmp($this->data, "\x37\x7A\xBC\xAF\x27\x1C", 6) === 0; - } - - /** - * Returns compression ratio as a float (0.0 to 1.0+), or null if unknown. - */ - public function getCompressionRatio(): ?float - { - if (! $this->parsed) { - $this->parse(); - } - - if ($this->totalUnpackedSize > 0 && $this->totalPackedSize > 0) { - return $this->totalPackedSize / $this->totalUnpackedSize; - } - - return null; - } - - private function parse(): void - { - $this->parsed = true; - - if ($this->len < 32) { // need at least fixed header - $this->lastError = 'Data too short for 7z header (need at least 32 bytes)'; - // Try fallback scanning even for very short data - $this->fallbackScanForFilenames(); - - return; - } - - // Signature check - if (strncmp($this->data, "\x37\x7A\xBC\xAF\x27\x1C", 6) !== 0) { - $this->lastError = 'Invalid 7z signature'; - // Try fallback scanning anyway - data might be partial - $this->fallbackScanForFilenames(); - - return; - } - - // Read header version - $majorVersion = ord($this->data[6]); - $minorVersion = ord($this->data[7]); - - // Read next header info - $nextHeaderOffset = $this->readUInt64LE(12); - $nextHeaderSize = $this->readUInt64LE(20); - $nextHeaderCRC = $this->readUInt32LE(28); - - // Scan the packed data region for compression method signatures - $this->scanForCompressionMethods(32, min(32 + $nextHeaderOffset, $this->len)); - - // Bounds sanity - if ($nextHeaderSize <= 0 || $nextHeaderSize > 16 * 1024 * 1024) { // cap 16MB header - $this->lastError = 'Invalid next header size: '.$nextHeaderSize; - $this->fallbackScanForFilenames(); - - return; - } - - $nextHeaderStart = 32 + $nextHeaderOffset; - $nextHeaderEnd = $nextHeaderStart + $nextHeaderSize; - - if ($nextHeaderEnd > $this->len) { - // Incomplete buffer - still try to parse what we have - $this->lastError = 'Incomplete archive data (header extends beyond buffer)'; - - // Try partial parsing with available data - if ($nextHeaderStart < $this->len) { - $this->parsePartialHeader((int) $nextHeaderStart, $this->len); - } - - // Also try fallback scanning - $this->fallbackScanForFilenames(); - - return; - } - - $cursor = (int) $nextHeaderStart; - - // First byte may be kEncodedHeader (unsupported) or kHeader - $id = ord($this->data[$cursor]); - - if ($id === self::K_ENCODED_HEADER) { - $this->encodedHeader = true; - - // Parse encoded header for compression info and encryption detection - $this->parseEncodedHeader($cursor, (int) $nextHeaderEnd); - - // Fallback scan for any readable filenames - $this->fallbackScanForFilenames(); - - return; - } - - if ($id !== self::K_HEADER) { - $this->lastError = 'Unexpected header ID: '.$id; - $this->fallbackScanForFilenames(); - - return; - } - - $cursor++; - - // Loop until K_END looking for various info blocks - while ($cursor < $nextHeaderEnd) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - break; - } - - switch ($id) { - case self::K_ARCHIVE_PROPERTIES: - $cursor = $this->skipUntilEnd($cursor, (int) $nextHeaderEnd); - break; - - case self::K_ADDITIONAL_STREAMS_INFO: - $cursor = $this->skipUntilEnd($cursor, (int) $nextHeaderEnd); - break; - - case self::K_MAIN_STREAMS_INFO: - $cursor = $this->parseMainStreamsInfo($cursor, (int) $nextHeaderEnd); - break; - - case self::K_FILES_INFO: - $cursor = $this->parseFilesInfo($cursor, (int) $nextHeaderEnd); - break; - - default: - // Try to skip unknown property - if ($this->readVIntAt($cursor, $propSize, $cursor, (int) $nextHeaderEnd)) { - $cursor += $propSize; - } else { - $cursor = -1; - } - break; - } - - if ($cursor === -1) { - break; - } - } - - // Build consolidated file info array - $this->buildFileInfo(); - - // If we didn't find any files, try fallback - if (empty($this->names)) { - $this->fallbackScanForFilenames(); - } - } - - /** - * Parse encoded header to extract compression method info and detect encryption. - */ - private function parseEncodedHeader(int $cursor, int $limit): void - { - $cursor++; // skip K_ENCODED_HEADER byte - - // Scan for AES encryption signature - $scan = substr($this->data, $cursor, min(1024, $limit - $cursor)); - if (strpos($scan, self::METHOD_AES) !== false) { - $this->encrypted = true; - $this->headerEncrypted = true; - } - - // Look for streams info - while ($cursor < $limit) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - break; - } - - if ($id === self::K_PACK_INFO) { - $cursor = $this->parsePackInfo($cursor, $limit); - } elseif ($id === self::K_UNPACK_INFO) { - $cursor = $this->parseUnpackInfo($cursor, $limit); - } elseif ($id === self::K_SUBSTREAMS_INFO) { - $cursor = $this->skipUntilEnd($cursor, $limit); - } else { - // Skip unknown - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - break; - } - $cursor += $propSize; - } - - if ($cursor === -1) { - break; - } - } - } - - /** - * Parse a partial/incomplete header to extract whatever information is available. - */ - private function parsePartialHeader(int $start, int $end): void - { - $cursor = $start; - - // Try to identify what kind of block this might be - if ($cursor < $end) { - $id = ord($this->data[$cursor]); - - if ($id === self::K_ENCODED_HEADER) { - $this->encodedHeader = true; - $scan = substr($this->data, $cursor, min(512, $end - $cursor)); - if (strpos($scan, self::METHOD_AES) !== false) { - $this->encrypted = true; - $this->headerEncrypted = true; - } - } elseif ($id === self::K_HEADER) { - $cursor++; - // Try to parse as much as possible - while ($cursor < $end - 1) { - $blockId = ord($this->data[$cursor]); - if ($blockId === self::K_FILES_INFO) { - $cursor++; - $this->parseFilesInfo($cursor, $end); - break; - } - $cursor++; - } - } - } - } - - /** - * Parse MainStreamsInfo to get packed/unpacked sizes and detect solid archives. - */ - private function parseMainStreamsInfo(int $cursor, int $limit): int - { - while ($cursor < $limit) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - return $cursor; - } - - switch ($id) { - case self::K_PACK_INFO: - $cursor = $this->parsePackInfo($cursor, $limit); - break; - - case self::K_UNPACK_INFO: - $cursor = $this->parseUnpackInfo($cursor, $limit); - break; - - case self::K_SUBSTREAMS_INFO: - $cursor = $this->parseSubstreamsInfo($cursor, $limit); - break; - - default: - return $this->skipUntilEnd($cursor - 1, $limit); - } - - if ($cursor === -1) { - return -1; - } - } - - return $cursor; - } - - /** - * Parse PackInfo to get packed sizes. - */ - private function parsePackInfo(int $cursor, int $limit): int - { - // PackPos (VInt) - if (! $this->readVIntAt($cursor, $packPos, $cursor, $limit)) { - return -1; - } - - // NumPackStreams (VInt) - if (! $this->readVIntAt($cursor, $numPackStreams, $cursor, $limit)) { - return -1; - } - - while ($cursor < $limit) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - return $cursor; - } - - if ($id === self::K_SIZE) { - // Read packed sizes - for ($i = 0; $i < $numPackStreams && $cursor < $limit; $i++) { - if ($this->readVIntAt($cursor, $size, $cursor, $limit)) { - $this->packedSizes[] = $size; - $this->totalPackedSize += $size; - } - } - } elseif ($id === self::K_CRC) { - // Skip CRC info - $cursor = $this->skipBitVector($cursor, $limit, $numPackStreams); - } else { - return -1; - } - } - - return $cursor; - } - - /** - * Parse UnpackInfo to get unpack sizes and detect compression methods. - */ - private function parseUnpackInfo(int $cursor, int $limit): int - { - while ($cursor < $limit) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - return $cursor; - } - - if ($id === self::K_FOLDER) { - $cursor = $this->parseFolderInfo($cursor, $limit); - } elseif ($id === self::K_CODERS_UNPACK_SIZE) { - $cursor = $this->parseCodersUnpackSize($cursor, $limit); - } elseif ($id === self::K_CRC) { - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - } else { - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - } - - if ($cursor === -1) { - return -1; - } - } - - return $cursor; - } - - /** - * Parse FolderInfo to detect compression methods and solid archive structure. - */ - private function parseFolderInfo(int $cursor, int $limit): int - { - // NumFolders (VInt) - if (! $this->readVIntAt($cursor, $numFolders, $cursor, $limit)) { - return -1; - } - - // If there's only one folder with multiple files, it's likely solid - if ($numFolders === 1 && $this->numFiles > 1) { - $this->solidArchive = true; - } - - // External flag - if ($cursor >= $limit) { - return -1; - } - $external = ord($this->data[$cursor]); - $cursor++; - - if ($external !== 0) { - // External data - skip - if (! $this->readVIntAt($cursor, $dataIndex, $cursor, $limit)) { - return -1; - } - - return $cursor; - } - - // Parse each folder - for ($i = 0; $i < $numFolders && $cursor < $limit; $i++) { - $cursor = $this->parseFolder($cursor, $limit); - if ($cursor === -1) { - return -1; - } - } - - return $cursor; - } - - /** - * Parse a single Folder structure to extract compression method info. - */ - private function parseFolder(int $cursor, int $limit): int - { - // NumCoders (VInt) - if (! $this->readVIntAt($cursor, $numCoders, $cursor, $limit)) { - return -1; - } - - $totalInputStreams = 0; - $totalOutputStreams = 0; - - for ($i = 0; $i < $numCoders && $cursor < $limit; $i++) { - // Coder flags - $flags = ord($this->data[$cursor]); - $cursor++; - - $codecIdSize = $flags & 0x0F; - $isComplex = ($flags & 0x10) !== 0; - $hasAttributes = ($flags & 0x20) !== 0; - - // Codec ID - if ($cursor + $codecIdSize > $limit) { - return -1; - } - $codecId = substr($this->data, $cursor, $codecIdSize); - $cursor += $codecIdSize; - - // Identify compression method - $method = $this->identifyCompressionMethod($codecId); - if ($method !== null) { - $this->compressionMethods[] = $method; - - // Check for AES encryption - if ($method === 'AES-256') { - $this->encrypted = true; - } - } - - if ($isComplex) { - // NumInStreams (VInt) - if (! $this->readVIntAt($cursor, $numIn, $cursor, $limit)) { - return -1; - } - // NumOutStreams (VInt) - if (! $this->readVIntAt($cursor, $numOut, $cursor, $limit)) { - return -1; - } - $totalInputStreams += $numIn; - $totalOutputStreams += $numOut; - } else { - $totalInputStreams++; - $totalOutputStreams++; - } - - if ($hasAttributes) { - // PropertiesSize (VInt) - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - } - } - - // BindPairs - $numBindPairs = $totalOutputStreams - 1; - for ($i = 0; $i < $numBindPairs && $cursor < $limit; $i++) { - if (! $this->readVIntAt($cursor, $inIndex, $cursor, $limit)) { - return -1; - } - if (! $this->readVIntAt($cursor, $outIndex, $cursor, $limit)) { - return -1; - } - } - - // PackedStreams - $numPackedStreams = $totalInputStreams - $numBindPairs; - if ($numPackedStreams > 1) { - for ($i = 0; $i < $numPackedStreams && $cursor < $limit; $i++) { - if (! $this->readVIntAt($cursor, $packedIndex, $cursor, $limit)) { - return -1; - } - } - } - - return $cursor; - } - - /** - * Identify compression method from codec ID bytes. - */ - private function identifyCompressionMethod(string $codecId): ?string - { - if ($codecId === self::METHOD_COPY || $codecId === "\x00") { - return 'Copy'; - } - if (str_starts_with($codecId, "\x03\x01\x01")) { - return 'LZMA'; - } - if ($codecId === self::METHOD_LZMA2 || str_starts_with($codecId, "\x21")) { - return 'LZMA2'; - } - if (str_starts_with($codecId, "\x03\x04\x01")) { - return 'PPMd'; - } - if (str_starts_with($codecId, "\x03\x03\x01\x03")) { - return 'BCJ'; - } - if (str_starts_with($codecId, "\x03\x03\x01\x1B")) { - return 'BCJ2'; - } - if (str_starts_with($codecId, "\x04\x01\x08")) { - return 'Deflate'; - } - if (str_starts_with($codecId, "\x04\x02\x02")) { - return 'BZip2'; - } - if (str_starts_with($codecId, "\x06\xF1\x07\x01")) { - return 'AES-256'; - } - if (str_starts_with($codecId, "\x03\x03\x01\x05")) { - return 'ARM'; - } - if (str_starts_with($codecId, "\x03\x03\x01\x08")) { - return 'SPARC'; - } - - return null; - } - - /** - * Parse CodersUnpackSize. - */ - private function parseCodersUnpackSize(int $cursor, int $limit): int - { - // This should read sizes for each output stream of each folder - // For now, just read available VInts as sizes - while ($cursor < $limit) { - $b = ord($this->data[$cursor]); - if ($b === self::K_END || $b === self::K_CRC) { - break; - } - if ($this->readVIntAt($cursor, $size, $newCursor, $limit)) { - $this->sizes[] = $size; - $this->totalUnpackedSize += $size; - $cursor = $newCursor; - } else { - break; - } - } - - return $cursor; - } - - /** - * Parse SubstreamsInfo for individual file sizes within folders. - */ - private function parseSubstreamsInfo(int $cursor, int $limit): int - { - while ($cursor < $limit) { - $id = ord($this->data[$cursor]); - $cursor++; - - if ($id === self::K_END) { - return $cursor; - } - - switch ($id) { - case self::K_NUM_UNPACK_STREAM: - // NumUnpackStreams per folder - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - break; - - case self::K_SIZE: - // Sizes - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - break; - - case self::K_CRC: - // CRCs - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - break; - - default: - if (! $this->readVIntAt($cursor, $propSize, $cursor, $limit)) { - return -1; - } - $cursor += $propSize; - break; - } - } - - return $cursor; - } - - /** - * Skip a bit vector structure. - */ - private function skipBitVector(int $cursor, int $limit, int $numItems): int - { - $allDefined = ord($this->data[$cursor]); - $cursor++; - - if ($allDefined === 0) { - // Bit array follows - $numBytes = (int) ceil($numItems / 8); - $cursor += $numBytes; - } - - // Then comes the actual CRC values - for ($i = 0; $i < $numItems && $cursor + 4 <= $limit; $i++) { - $cursor += 4; - } - - return $cursor; - } - - 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 > 100000) { // sanity cap - $this->lastError = 'Invalid number of files: '.$numFiles; - - return $limit; - } - $cursor = $newCursor; - $this->numFiles = $numFiles; - - // Initialize arrays for metadata - $names = []; - $emptyStreams = []; - $emptyFiles = []; - $antiFiles = []; - - // Property loop until K_END - 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; - } - - $propStart = $cursor; - - switch ($propId) { - case self::K_EMPTY_STREAM: - $emptyStreams = $this->parseBitVector($cursor, $propSize, $numFiles); - $cursor = $propStart + $propSize; - break; - - case self::K_EMPTY_FILE: - $numEmpty = count(array_filter($emptyStreams)); - $emptyFiles = $this->parseBitVector($cursor, $propSize, $numEmpty); - $cursor = $propStart + $propSize; - break; - - case self::K_ANTI: - $numEmpty = count(array_filter($emptyStreams)); - $antiFiles = $this->parseBitVector($cursor, $propSize, $numEmpty); - $cursor = $propStart + $propSize; - break; - - case self::K_NAME: - $names = $this->parseNames($cursor, $propSize, $numFiles); - $cursor = $propStart + $propSize; - break; - - case self::K_MTIME: - $this->mtimes = $this->parseFileTimes($cursor, $propSize, $numFiles); // @phpstan-ignore assign.propertyType - $cursor = $propStart + $propSize; - break; - - case self::K_CTIME: - $this->ctimes = $this->parseFileTimes($cursor, $propSize, $numFiles); // @phpstan-ignore assign.propertyType - $cursor = $propStart + $propSize; - break; - - case self::K_ATIME: - $this->atimes = $this->parseFileTimes($cursor, $propSize, $numFiles); // @phpstan-ignore assign.propertyType - $cursor = $propStart + $propSize; - break; - - case self::K_WIN_ATTRIBUTES: - $this->attributes = $this->parseAttributes($cursor, $propSize, $numFiles); // @phpstan-ignore assign.propertyType - $cursor = $propStart + $propSize; - break; - - case self::K_CRC: - $this->crcs = $this->parseCRCs($cursor, $propSize, $numFiles); // @phpstan-ignore assign.propertyType - $cursor = $propStart + $propSize; - break; - - default: - // Skip unknown property - $cursor = $propStart + $propSize; - break; - } - } - - // Assign collected names - if (! empty($names)) { - $this->names = array_values(array_unique($names)); // @phpstan-ignore assign.propertyType - } - - return $cursor; - } - - /** - * Parse file names from K_NAME property. - * - * @return array - */ - private function parseNames(int $cursor, int $propSize, int $numFiles): array - { - $names = []; - - if ($propSize < 1) { - return $names; - } - - $external = ord($this->data[$cursor]); - if ($external !== 0) { // External data not supported - return $names; - } - - $nameBytes = $propSize - 1; - $cursor++; - - if ($nameBytes <= 0) { - return $names; - } - - $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); - if ($utf8 === false) { - continue; - } - - $utf8 = trim($utf8); - if ($utf8 === '') { - continue; - } - - // Normalize path separators - $utf8Clean = str_replace(['\\'], '/', $utf8); - // Remove leading './' - $utf8Clean = preg_replace('#^\./#', '', $utf8Clean); - - if ($utf8Clean === '' || substr_count($utf8Clean, '/') > 16) { // excessive depth -> skip - continue; - } - - $names[] = $utf8Clean; - - if (count($names) >= $numFiles) { - break; - } - } - - return $names; - } - - /** - * Parse a bit vector from property data. - * - * @return list - */ - private function parseBitVector(int $cursor, int $propSize, int $numItems): array - { - $result = array_fill(0, $numItems, false); - - if ($propSize < 1) { - return $result; - } - - $allDefined = ord($this->data[$cursor]); - $cursor++; - - if ($allDefined !== 0) { - // All items are defined - return array_fill(0, $numItems, true); - } - - // Parse bit array - $numBytes = (int) ceil($numItems / 8); - for ($i = 0; $i < $numItems; $i++) { - $byteIndex = (int) ($i / 8); - $bitIndex = 7 - ($i % 8); - if ($cursor + $byteIndex < $this->len) { - $byte = ord($this->data[$cursor + $byteIndex]); - $result[$i] = (($byte >> $bitIndex) & 1) === 1; - } - } - - return $result; - } - - /** - * Parse file times from property data. - * - * @return array, bool> - */ - private function parseFileTimes(int $cursor, int $propSize, int $numFiles): array - { - $times = []; - - if ($propSize < 1) { - return $times; - } - - // Check for AllDefined byte - $allDefined = ord($this->data[$cursor]); - $cursor++; - - $definedBits = []; - if ($allDefined === 0) { - // Parse bit vector for which files have times defined - $numBytes = (int) ceil($numFiles / 8); - for ($i = 0; $i < $numFiles; $i++) { - $byteIndex = (int) ($i / 8); - $bitIndex = 7 - ($i % 8); - if ($cursor + $byteIndex < $this->len) { - $byte = ord($this->data[$cursor + $byteIndex]); - $definedBits[$i] = (($byte >> $bitIndex) & 1) === 1; - } else { - $definedBits[$i] = false; - } - } - $cursor += $numBytes; - } else { - $definedBits = array_fill(0, $numFiles, true); - } - - // External flag - if ($cursor >= $this->len) { - return $times; - } - $external = ord($this->data[$cursor]); - $cursor++; - - if ($external !== 0) { - return $times; // External data not supported - } - - // Read times for defined files (FILETIME format - 8 bytes each) - for ($i = 0; $i < $numFiles; $i++) { - if (! empty($definedBits[$i]) && $cursor + 8 <= $this->len) { - $filetime = $this->readUInt64LE($cursor); - // Convert FILETIME (100-ns intervals since 1601) to Unix timestamp - $times[$i] = $this->filetimeToUnix($filetime); - $cursor += 8; - } else { - $times[$i] = null; - } - } - - return $times; - } - - /** - * Parse Windows attributes from property data. - * - * @return array, int|null> - */ - private function parseAttributes(int $cursor, int $propSize, int $numFiles): array - { - $attrs = []; - - if ($propSize < 1) { - return $attrs; - } - - // AllDefined byte - $allDefined = ord($this->data[$cursor]); - $cursor++; - - $definedBits = []; - if ($allDefined === 0) { - $numBytes = (int) ceil($numFiles / 8); - for ($i = 0; $i < $numFiles; $i++) { - $byteIndex = (int) ($i / 8); - $bitIndex = 7 - ($i % 8); - if ($cursor + $byteIndex < $this->len) { - $byte = ord($this->data[$cursor + $byteIndex]); - $definedBits[$i] = (($byte >> $bitIndex) & 1) === 1; - } else { - $definedBits[$i] = false; - } - } - $cursor += $numBytes; - } else { - $definedBits = array_fill(0, $numFiles, true); - } - - // External flag - if ($cursor >= $this->len) { - return $attrs; - } - $external = ord($this->data[$cursor]); - $cursor++; - - if ($external !== 0) { - return $attrs; - } - - // Read attributes (4 bytes each) - for ($i = 0; $i < $numFiles; $i++) { - if (! empty($definedBits[$i]) && $cursor + 4 <= $this->len) { - $attrs[$i] = $this->readUInt32LE($cursor); - $cursor += 4; - } else { - $attrs[$i] = null; - } - } - - return $attrs; - } - - /** - * Parse CRC values from property data. - * - * @return array - */ - private function parseCRCs(int $cursor, int $propSize, int $numFiles): array - { - $crcs = []; - - if ($propSize < 1) { - return $crcs; - } - - // AllDefined byte - $allDefined = ord($this->data[$cursor]); - $cursor++; - - $definedBits = []; - if ($allDefined === 0) { - $numBytes = (int) ceil($numFiles / 8); - for ($i = 0; $i < $numFiles; $i++) { - $byteIndex = (int) ($i / 8); - $bitIndex = 7 - ($i % 8); - if ($cursor + $byteIndex < $this->len) { - $byte = ord($this->data[$cursor + $byteIndex]); - $definedBits[$i] = (($byte >> $bitIndex) & 1) === 1; - } else { - $definedBits[$i] = false; - } - } - $cursor += $numBytes; - } else { - $definedBits = array_fill(0, $numFiles, true); - } - - // Read CRC values (4 bytes each) - for ($i = 0; $i < $numFiles; $i++) { - if (! empty($definedBits[$i]) && $cursor + 4 <= $this->len) { - $crc = $this->readUInt32LE($cursor); - $crcs[$i] = sprintf('%08X', $crc); - $cursor += 4; - } else { - $crcs[$i] = null; - } - } - - return $crcs; - } - - /** - * Build consolidated file info array. - */ - private function buildFileInfo(): void - { - $numFiles = max(count($this->names), $this->numFiles); - - for ($i = 0; $i < $numFiles; $i++) { - $isDir = false; - if (isset($this->attributes[$i])) { - $isDir = ($this->attributes[$i] & self::FILE_ATTRIBUTE_DIRECTORY) !== 0; - } - - $this->files[$i] = [ - 'name' => $this->names[$i] ?? null, - 'size' => $this->sizes[$i] ?? null, - 'packed_size' => $this->packedSizes[$i] ?? null, - 'crc' => $this->crcs[$i] ?? null, - 'attributes' => $this->attributes[$i] ?? null, - 'is_dir' => $isDir, - 'mtime' => $this->mtimes[$i] ?? null, - 'ctime' => $this->ctimes[$i] ?? null, - 'atime' => $this->atimes[$i] ?? null, - ]; - } - } - - /** - * Scan raw data for compression method signatures. - */ - private function scanForCompressionMethods(int $start, int $end): void - { - $data = substr($this->data, $start, $end - $start); - - // Look for common method signatures - if (strpos($data, self::METHOD_AES) !== false) { - $this->encrypted = true; - $this->compressionMethods[] = 'AES-256'; - } - if (strpos($data, self::METHOD_LZMA2) !== false) { - $this->compressionMethods[] = 'LZMA2'; - } - if (strpos($data, self::METHOD_LZMA) !== false) { - $this->compressionMethods[] = 'LZMA'; - } - if (strpos($data, self::METHOD_PPMD) !== false) { - $this->compressionMethods[] = 'PPMd'; - } - if (strpos($data, self::METHOD_BZIP2) !== false) { - $this->compressionMethods[] = 'BZip2'; - } - if (strpos($data, self::METHOD_DEFLATE) !== false) { - $this->compressionMethods[] = 'Deflate'; - } - } - - /** - * Fallback scanning for filenames when normal parsing fails. - * Attempts to find UTF-16LE encoded filenames in the raw data. - */ - private function fallbackScanForFilenames(): void - { - if (! empty($this->names)) { - return; // Already have names - } - - // Look for common file extension patterns in UTF-16LE - $patterns = [ - '.avi', '.mkv', '.mp4', '.wmv', '.mov', - '.mp3', '.flac', '.wav', '.ogg', - '.rar', '.zip', '.exe', '.dll', - '.nfo', '.txt', '.pdf', '.doc', - '.jpg', '.png', '.gif', '.bmp', - '.iso', '.bin', '.img', '.nrg', - ]; - - $found = []; - - foreach ($patterns as $ext) { - // Convert extension to UTF-16LE for searching - $utf16Ext = @iconv('UTF-8', 'UTF-16LE', $ext); - if ($utf16Ext === false) { - continue; - } - - $pos = 0; - while (($pos = strpos($this->data, $utf16Ext, $pos)) !== false) { - // Try to extract the full filename by looking backwards - $start = $this->findFilenameStart($pos); - if ($start !== false && $start < $pos) { - $len = ($pos - $start) + strlen($utf16Ext); - $nameData = substr($this->data, $start, $len); - - // Validate it's likely a filename (reasonable length, no control chars) - if (strlen($nameData) > 2 && strlen($nameData) < 512) { - $utf8 = @iconv('UTF-16LE', 'UTF-8//IGNORE', $nameData); - if ($utf8 !== false && $this->isValidFilename($utf8)) { - $utf8 = str_replace('\\', '/', $utf8); - $found[] = $utf8; - } - } - } - $pos += strlen($utf16Ext); - } - } - - // Also try to find NFO files specifically (common in releases) - $this->scanForNfoFiles($found); // @phpstan-ignore argument.type - - if (! empty($found)) { - $this->names = array_values(array_unique($found)); // @phpstan-ignore assign.propertyType - } - } - - /** - * Find the start of a UTF-16LE filename by scanning backwards. - */ - private function findFilenameStart(int $extensionPos): int|false - { - // Scan backwards looking for a null terminator or invalid char - $start = $extensionPos; - $maxLen = 256; // Max filename length to consider - - for ($i = $extensionPos - 2; $i >= max(0, $extensionPos - $maxLen * 2); $i -= 2) { - if ($i + 1 >= $this->len) { - continue; - } - - $lo = ord($this->data[$i]); - $hi = ord($this->data[$i + 1]); - - // Check for null terminator - if ($lo === 0 && $hi === 0) { - $start = $i + 2; - break; - } - - // Check for invalid filename characters - if ($hi === 0) { - // ASCII range - check for path separators or invalid chars - if ($lo === 0 || $lo < 32) { - $start = $i + 2; - break; - } - } - - $start = $i; - } - - return $start >= 0 ? $start : false; - } - - /** - * Validate if a string looks like a valid filename. - */ - private function isValidFilename(string $name): bool - { - $name = trim($name); - - if (empty($name) || strlen($name) > 260) { - return false; - } - - // Must have at least one printable character - if (! preg_match('/[a-zA-Z0-9]/', $name)) { - return false; - } - - // Should not have too many special characters - $specialCount = preg_match_all('/[^\w\s.\-_\/\\\\]/', $name); - if ($specialCount > 5) { - return false; - } - - return true; - } - - /** - * Scan for NFO files which are very common in release archives. - * - * @param array $found - */ - private function scanForNfoFiles(array &$found): void - { - // NFO files are extremely common and usually have simple names - $nfoMarker = @iconv('UTF-8', 'UTF-16LE', '.nfo'); - if ($nfoMarker === false) { - return; - } - - $pos = 0; - while (($pos = strpos($this->data, $nfoMarker, $pos)) !== false) { - $start = $this->findFilenameStart($pos); - if ($start !== false && $start < $pos) { - $len = ($pos - $start) + strlen($nfoMarker); - $nameData = substr($this->data, $start, $len); - - if (strlen($nameData) > 2 && strlen($nameData) < 256) { - $utf8 = @iconv('UTF-16LE', 'UTF-8//IGNORE', $nameData); - if ($utf8 !== false && $this->isValidFilename($utf8)) { - $found[] = str_replace('\\', '/', $utf8); // @phpstan-ignore parameterByRef.type - } - } - } - $pos += strlen($nfoMarker); - } - } - - /** - * Convert Windows FILETIME to Unix timestamp. - */ - private function filetimeToUnix(int $filetime): int - { - // FILETIME is 100-nanosecond intervals since January 1, 1601 - // Unix epoch is January 1, 1970 - // Difference is 116444736000000000 100-ns intervals - $unixEpochDiff = 116444736000000000; - - if ($filetime <= $unixEpochDiff) { - return 0; - } - - return (int) (($filetime - $unixEpochDiff) / 10000000); - } - - 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. - * - * @param-out int $value - */ - 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; - } - - /** - * Read a 32-bit unsigned little-endian integer. - */ - private function readUInt32LE(int $offset): int - { - if ($offset + 4 > $this->len) { - return 0; - } - - return ord($this->data[$offset]) - | (ord($this->data[$offset + 1]) << 8) - | (ord($this->data[$offset + 2]) << 16) - | (ord($this->data[$offset + 3]) << 24); - } - - /** - * Extract a summary of archive information as an associative array. - * Useful for quick inspection of archive contents. - * - * @return array - */ - public function getSummary(): array - { - if (! $this->parsed) { - $this->parse(); - } - - $dirs = 0; - $regularFiles = 0; - - foreach ($this->files as $file) { - if ($file['is_dir']) { - $dirs++; - } else { - $regularFiles++; - } - } - - return [ - 'valid_signature' => $this->isValid7zSignature(), - 'file_count' => $this->numFiles, - 'directory_count' => $dirs, - 'regular_file_count' => $regularFiles, - 'total_unpacked_size' => $this->totalUnpackedSize, - 'total_packed_size' => $this->totalPackedSize, - 'compression_ratio' => $this->getCompressionRatio(), - 'compression_methods' => $this->getCompressionMethods(), - 'encrypted' => $this->encrypted, - 'header_encrypted' => $this->headerEncrypted, - 'encoded_header' => $this->encodedHeader, - 'solid_archive' => $this->solidArchive, - 'last_error' => $this->lastError, - ]; - } - - /** - * Get files matching a specific extension. - * - * @return array> - */ - public function getFilesByExtension(string $extension): array - { - if (! $this->parsed) { - $this->parse(); - } - - $extension = ltrim(strtolower($extension), '.'); - $matches = []; - - foreach ($this->files as $index => $file) { - if ($file['name'] === null) { - continue; - } - - $fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); - if ($fileExt === $extension) { - $matches[$index] = $file; - } - } - - return $matches; - } - - /** - * Check if archive contains any files with given extension. - * - * @return array - */ - public function hasFileWithExtension(string $extension): bool - { - return ! empty($this->getFilesByExtension($extension)); - } - - /** - * Get all directory entries from the archive. - * - * @return array - */ - public function getDirectories(): array - { - if (! $this->parsed) { - $this->parse(); - } - - return array_filter($this->files, fn ($file) => $file['is_dir']); - } - - /** - * Get the largest file in the archive. - * - * @return array - */ - public function getLargestFile(): ?array - { - if (! $this->parsed) { - $this->parse(); - } - - $largest = null; - $maxSize = -1; - - foreach ($this->files as $file) { - if ($file['size'] !== null && $file['size'] > $maxSize && ! $file['is_dir']) { - $maxSize = $file['size']; - $largest = $file; - } - } - - return $largest; - } -} diff --git a/config/nntmux_settings.php b/config/nntmux_settings.php index d9c6c9ce1..928852aee 100644 --- a/config/nntmux_settings.php +++ b/config/nntmux_settings.php @@ -3,7 +3,6 @@ return [ 'unrar_path' => env('UNRAR_PATH', '/usr/bin/unrar'), 'unzip_path' => env('UNZIP_PATH', '/usr/bin/unzip'), - '7zip_path' => env('S7ZIP_PATH', '/usr/bin/7z'), 'check_passworded_rars' => env('CHECK_PASSWORDED_RARS', false), 'delete_passworded_releases' => env('DELETE_PASSWORDED_RELEASES', false), 'delete_possible_passworded_releases' => env('DELETE_POSSIBLE_PASSWORDED_RELEASES', false), diff --git a/docker/8.4/Dockerfile b/docker/8.4/Dockerfile index 39316ef12..970680fbf 100644 --- a/docker/8.4/Dockerfile +++ b/docker/8.4/Dockerfile @@ -5,7 +5,6 @@ LABEL maintainer="Fossil01" ARG WWWGROUP ARG NODE_VERSION=22 ARG POSTGRES_VERSION=17 -ARG SEVENZIP_VERSION=2409 ARG MYSQL_CLIENT="mariadb-client" WORKDIR /var/www/html @@ -13,7 +12,7 @@ WORKDIR /var/www/html ENV DEBIAN_FRONTEND noninteractive RUN apt-get update \ - && apt-get install -y gnupg gosu curl ca-certificates zip unzip unrar 7zip lame git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip unrar lame git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils \ && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_nginx.list \ @@ -45,18 +44,6 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Determine ARCH and download and extract the appropriate version of 7-Zip -RUN ARCH="$(dpkg --print-architecture)" && \ - if [ "$ARCH" = "amd64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-x64.tar.xz"; \ - fi && \ - if [ "$ARCH" = "arm64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-arm64.tar.xz"; \ - fi && \ - wget "$SZIP_URL" -O /tmp/7z.tar.xz && \ - tar -xf /tmp/7z.tar.xz -C /tmp/ && \ - mv /tmp/7zz /usr/bin/7zz && \ - rm -f /tmp/7z.tar.xz && rm -f /tmp/7zzs RUN groupadd --force -g $WWWGROUP sail RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail diff --git a/docker/8.5/Dockerfile b/docker/8.5/Dockerfile index 41c323d0e..8d9b48d66 100644 --- a/docker/8.5/Dockerfile +++ b/docker/8.5/Dockerfile @@ -5,7 +5,6 @@ LABEL maintainer="Fossil01" ARG WWWGROUP ARG NODE_VERSION=22 ARG POSTGRES_VERSION=17 -ARG SEVENZIP_VERSION=2409 ARG MYSQL_CLIENT="mariadb-client" WORKDIR /var/www/html @@ -14,7 +13,7 @@ ENV DEBIAN_FRONTEND noninteractive # Install base packages and add repositories RUN apt-get update \ - && apt-get install -y gnupg gosu curl ca-certificates zip unzip unrar 7zip lame git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip unrar lame git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils \ && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_nginx.list \ @@ -67,18 +66,6 @@ RUN apt-get install -y yarn $MYSQL_CLIENT postgresql-client-$POSTGRES_VERSION \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Determine ARCH and download and extract the appropriate version of 7-Zip -RUN ARCH="$(dpkg --print-architecture)" && \ - if [ "$ARCH" = "amd64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-x64.tar.xz"; \ - fi && \ - if [ "$ARCH" = "arm64" ]; then \ - SZIP_URL="https://www.7-zip.org/a/7z$SEVENZIP_VERSION-linux-arm64.tar.xz"; \ - fi && \ - wget "$SZIP_URL" -O /tmp/7z.tar.xz && \ - tar -xf /tmp/7z.tar.xz -C /tmp/ && \ - mv /tmp/7zz /usr/bin/7zz && \ - rm -f /tmp/7z.tar.xz && rm -f /tmp/7zzs RUN groupadd --force -g $WWWGROUP sail RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail