| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib\Gitler; |
| 39 | |
| 40 | use Std\Lib\Git\GitException; |
| 41 | use Std\Lib\Git\Repository; |
| 42 | use Std\Lib\Git\Tree; |
| 43 | |
| 44 | /** |
| 45 | * リポジトリのアーカイブ(zip、tar、tar.gz)。git archiveと同じく、前に「名前-リビジョン/」を付ける |
| 46 | */ |
| 47 | final class Archive { |
| 48 | public const FORMATS = ['zip' => 'application/zip', 'tar.gz' => 'application/gzip', 'tar' => 'application/x-tar']; |
| 49 | |
| 50 | /** |
| 51 | * @param string $treeish コミット(時刻に使う)又はツリー |
| 52 | * @param string $format zip、tar、tar.gz |
| 53 | * @param string $prefix 例:「repo-main/」 |
| 54 | * @param callable $out fn(string $bytes) |
| 55 | */ |
| 56 | public static function write(Repository $repo, string $treeish, string $format, string $prefix, callable $out): void { |
| 57 | if (!isset(self::FORMATS[$format])) throw new GitException("未対応の形式:{$format}"); |
| 58 | $tree = $repo->treeOf($treeish) ?? throw new GitException("リビジョンが有りません:{$treeish}"); |
| 59 | $commit = $repo->peel($repo->isId($treeish) ? $treeish : ($repo->resolve($treeish) ?? ''), 'commit'); |
| 60 | $mtime = $commit !== null ? (int)$repo->commit($commit)->committer?->time : time(); |
| 61 | $entries = []; |
| 62 | self::collect($repo, $tree, $prefix, $entries); |
| 63 | if ($format === 'zip') { |
| 64 | self::zip($entries, $mtime, $out, $repo); |
| 65 | return; |
| 66 | } |
| 67 | if ($format === 'tar.gz') { |
| 68 | $ctx = deflate_init(ZLIB_ENCODING_GZIP, ['level' => 6]); |
| 69 | $raw = $out; |
| 70 | $out = function(string $b) use ($ctx, $raw): void { |
| 71 | $z = deflate_add($ctx, $b, ZLIB_NO_FLUSH); |
| 72 | if ($z !== '') $raw($z); |
| 73 | }; |
| 74 | self::tar($entries, $mtime, $out, $repo); |
| 75 | $raw(deflate_add($ctx, '', ZLIB_FINISH)); |
| 76 | return; |
| 77 | } |
| 78 | self::tar($entries, $mtime, $out, $repo); |
| 79 | } |
| 80 | |
| 81 | /** [mode, パス, ID](ディレクトリは最後が「/」) */ |
| 82 | private static function collect(Repository $repo, string $tree, string $prefix, array &$entries): void { |
| 83 | if ($prefix !== '') $entries[] = [Tree::DIR, $prefix, $tree]; |
| 84 | foreach ($repo->tree($tree)->entries as [$mode, $name, $id]) { |
| 85 | if ($mode === Tree::DIR) self::collect($repo, $id, $prefix.$name.'/', $entries); |
| 86 | else if ($mode !== Tree::SUBMODULE) $entries[] = [$mode, $prefix.$name, $id]; |
| 87 | else $entries[] = [Tree::DIR, $prefix.$name.'/', $id]; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | private static function tar(array $entries, int $mtime, callable $out, Repository $repo): void { |
| 92 | foreach ($entries as [$mode, $path, $id]) { |
| 93 | $isDir = $mode === Tree::DIR; |
| 94 | $isLink = $mode === Tree::LINK; |
| 95 | $data = $isDir ? '' : $repo->blob($id); |
| 96 | $perm = $isDir || $mode === Tree::EXEC ? 0775 : ($isLink ? 0777 : 0664); |
| 97 | $link = $isLink ? $data : ''; |
| 98 | $size = $isDir || $isLink ? 0 : strlen($data); |
| 99 | // 長い名前はpaxの拡張ヘッダー |
| 100 | [$name, $pre] = self::splitName($path); |
| 101 | if ($name === null || strlen($link) > 100) { |
| 102 | $pax = self::paxRecord('path', $path).($isLink && strlen($link) > 100 ? self::paxRecord('linkpath', $link) : ''); |
| 103 | $out(self::tarHeader('././@PaxHeader', '', 0644, strlen($pax), $mtime, 'x', '').$pax.str_repeat("\0", (512 - strlen($pax) % 512) % 512)); |
| 104 | [$name, $pre] = [substr(basename($path), 0, 100), '']; |
| 105 | } |
| 106 | $out(self::tarHeader($name, $pre, $perm, $size, $mtime, $isDir ? '5' : ($isLink ? '2' : '0'), substr($link, 0, 100))); |
| 107 | if ($size > 0) $out($data.str_repeat("\0", (512 - $size % 512) % 512)); |
| 108 | } |
| 109 | $out(str_repeat("\0", 1024)); |
| 110 | } |
| 111 | |
| 112 | /** ustarの名前(100)と前(155)に分ける。入らなければ[null, null] */ |
| 113 | private static function splitName(string $path): array { |
| 114 | if (strlen($path) <= 100) return [$path, '']; |
| 115 | for ($i = strlen($path) - 1; $i > 0; $i--) { |
| 116 | if ($path[$i] !== '/' || $i > 155) continue; |
| 117 | if (strlen($path) - $i - 1 <= 100 && strlen($path) - $i - 1 > 0) return [substr($path, $i + 1), substr($path, 0, $i)]; |
| 118 | } |
| 119 | return [null, null]; |
| 120 | } |
| 121 | |
| 122 | private static function paxRecord(string $key, string $value): string { |
| 123 | $body = " {$key}={$value}\n"; |
| 124 | $len = strlen($body); |
| 125 | $n = $len + strlen((string)$len); |
| 126 | if (strlen((string)$n) !== strlen((string)$len)) $n++; |
| 127 | return $n.$body; |
| 128 | } |
| 129 | |
| 130 | private static function tarHeader(string $name, string $prefix, int $mode, int $size, int $mtime, string $type, string $link): string { |
| 131 | $h = str_pad($name, 100, "\0").sprintf('%07o', $mode)."\0".sprintf('%07o', 0)."\0".sprintf('%07o', 0)."\0" |
| 132 | .sprintf('%011o', $size)."\0".sprintf('%011o', $mtime)."\0".' '.$type.str_pad($link, 100, "\0") |
| 133 | ."ustar\0".'00'.str_pad('git', 32, "\0").str_pad('git', 32, "\0").sprintf('%07o', 0)."\0".sprintf('%07o', 0)."\0" |
| 134 | .str_pad($prefix, 155, "\0"); |
| 135 | $h = str_pad($h, 512, "\0"); |
| 136 | $sum = array_sum(array_map('ord', str_split($h))); |
| 137 | return substr($h, 0, 148).sprintf('%06o', $sum)."\0 ".substr($h, 156); |
| 138 | } |
| 139 | |
| 140 | private static function zip(array $entries, int $mtime, callable $out, Repository $repo): void { |
| 141 | $d = getdate($mtime); |
| 142 | $dosTime = ($d['hours'] << 11) | ($d['minutes'] << 5) | intdiv($d['seconds'], 2); |
| 143 | $dosDate = ((max(1980, $d['year']) - 1980) << 9) | ($d['mon'] << 5) | $d['mday']; |
| 144 | $central = ''; |
| 145 | $offset = 0; |
| 146 | $count = 0; |
| 147 | foreach ($entries as [$mode, $path, $id]) { |
| 148 | $isDir = $mode === Tree::DIR; |
| 149 | $data = $isDir ? '' : $repo->blob($id); |
| 150 | $crc = $isDir ? 0 : crc32($data); |
| 151 | $method = $isDir || strlen($data) < 64 ? 0 : 8; |
| 152 | $body = $method === 8 ? (string)gzdeflate($data, 6) : $data; |
| 153 | if ($method === 8 && strlen($body) >= strlen($data)) { |
| 154 | $method = 0; |
| 155 | $body = $data; |
| 156 | } |
| 157 | if ($offset > 0xffffffff || strlen($data) > 0xffffffff) throw new GitException('アーカイブが大き過ぎます(4GB)'); |
| 158 | $unix = $isDir ? 040755 : ($mode === Tree::LINK ? 0120777 : ($mode === Tree::EXEC ? 0100755 : 0100644)); |
| 159 | // UTF-8の名前(ビット11) |
| 160 | $local = pack('VvvvvvVVVvv', 0x04034b50, 20, 0x0800, $method, $dosTime, $dosDate, $crc, strlen($body), strlen($data), strlen($path), 0).$path; |
| 161 | $central .= pack('VvvvvvvVVVvvvvvVV', 0x02014b50, (3 << 8) | 20, 20, 0x0800, $method, $dosTime, $dosDate, $crc, strlen($body), strlen($data), strlen($path), 0, 0, 0, 0, |
| 162 | ($unix << 16) | ($isDir ? 0x10 : 0), $offset).$path; |
| 163 | $out($local.$body); |
| 164 | $offset += strlen($local) + strlen($body); |
| 165 | $count++; |
| 166 | } |
| 167 | if ($count > 0xffff) throw new GitException('ファイルが多過ぎます(65535)'); |
| 168 | $out($central.pack('VvvvvVVv', 0x06054b50, 0, 0, $count, $count, strlen($central), $offset, 0)); |
| 169 | } |
| 170 | } |