<?php

namespace Core\Lib;

class SafeZip
{
    public static function extract(\ZipArchive $zip, $targetDir, $maxBytes = 536870912, $maxEntries = 10000)
    {
        $targetDir = rtrim((string) $targetDir, '/\\');
        if ($targetDir === '' || (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir))) {
            throw new \RuntimeException('无法创建解压目录');
        }
        if ($zip->numFiles > $maxEntries) {
            throw new \RuntimeException('压缩包文件数量超出限制');
        }

        $totalBytes = 0;
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $stat = $zip->statIndex($i);
            $name = str_replace('\\', '/', (string) ($stat['name'] ?? ''));
            self::assertSafeName($name);
            if (self::isSymlink($stat)) {
                throw new \RuntimeException('压缩包包含不允许的符号链接');
            }

            $size = max(0, intval($stat['size'] ?? 0));
            $totalBytes += $size;
            if ($totalBytes > $maxBytes) {
                throw new \RuntimeException('压缩包展开大小超出限制');
            }

            $destination = $targetDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, rtrim($name, '/'));
            if (substr($name, -1) === '/') {
                if (!is_dir($destination) && !mkdir($destination, 0755, true) && !is_dir($destination)) {
                    throw new \RuntimeException('无法创建压缩包目录');
                }
                continue;
            }

            $parent = dirname($destination);
            if (!is_dir($parent) && !mkdir($parent, 0755, true) && !is_dir($parent)) {
                throw new \RuntimeException('无法创建压缩包文件目录');
            }
            $source = $zip->getStream((string) ($stat['name'] ?? ''));
            $target = fopen($destination, 'wb');
            if (!$source || !$target) {
                if (is_resource($source)) fclose($source);
                if (is_resource($target)) fclose($target);
                throw new \RuntimeException('无法写入压缩包文件');
            }
            $written = stream_copy_to_stream($source, $target, $size + 1);
            fclose($source);
            fclose($target);
            if ($written === false || intval($written) !== $size) {
                @unlink($destination);
                throw new \RuntimeException('压缩包文件写入不完整');
            }
        }

        return true;
    }

    private static function assertSafeName($name)
    {
        if ($name === '' || strpos($name, "\0") !== false || $name[0] === '/' || preg_match('/^[A-Za-z]:\//', $name)) {
            throw new \RuntimeException('压缩包包含非法路径');
        }
        foreach (explode('/', trim($name, '/')) as $segment) {
            if ($segment === '' || $segment === '.' || $segment === '..') {
                throw new \RuntimeException('压缩包包含非法路径');
            }
        }
    }

    private static function isSymlink(array $stat)
    {
        $attributes = intval($stat['external_attributes'] ?? 0);
        return (($attributes >> 16) & 0xF000) === 0xA000;
    }
}
