496 lines
15 KiB
PHP
496 lines
15 KiB
PHP
<?php
|
|
|
|
function git_object_store_uint32($buffer, $offset) {
|
|
$value = unpack('Nvalue', substr($buffer, $offset, 4));
|
|
return $value['value'];
|
|
}
|
|
|
|
function git_object_store_repository_format($git_path) {
|
|
$config_path = get_safe_file_path($git_path, '/config');
|
|
if ($config_path === FALSE) {
|
|
return 'sha1';
|
|
}
|
|
|
|
$config = @parse_ini_file($config_path, TRUE, INI_SCANNER_RAW);
|
|
if ($config === FALSE) {
|
|
return FALSE;
|
|
}
|
|
|
|
foreach ($config as $section => $values) {
|
|
if (strcasecmp($section, 'extensions') !== 0) {
|
|
continue;
|
|
}
|
|
foreach ($values as $name => $value) {
|
|
if (strcasecmp($name, 'objectFormat') === 0) {
|
|
return strtolower(trim($value));
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'sha1';
|
|
}
|
|
|
|
function git_object_store_create($git_path) {
|
|
if (git_object_store_repository_format($git_path) !== 'sha1') {
|
|
return FALSE;
|
|
}
|
|
|
|
$store = array(
|
|
'path' => $git_path,
|
|
'objects' => array(),
|
|
'locations' => array(),
|
|
'pack_offsets' => array(),
|
|
'pack_objects' => array());
|
|
|
|
$pack_dir = get_safe_dir_path($git_path, '/objects/pack');
|
|
if ($pack_dir === FALSE) {
|
|
return $store;
|
|
}
|
|
|
|
$entries = @scandir($pack_dir);
|
|
if ($entries === FALSE) {
|
|
return $store;
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
if (!preg_match('~^(pack-[0-9a-f]{40})\.idx$~D', $entry, $matches)) {
|
|
continue;
|
|
}
|
|
|
|
$pack_name = $matches[1].'.pack';
|
|
$idx_path = get_safe_file_path($git_path, '/objects/pack/'.$entry);
|
|
$pack_path = get_safe_file_path($git_path, '/objects/pack/'.$pack_name);
|
|
if ($idx_path !== FALSE && $pack_path !== FALSE) {
|
|
git_object_store_load_index($store, $idx_path, $pack_path);
|
|
}
|
|
}
|
|
|
|
return $store;
|
|
}
|
|
|
|
function git_object_store_load_index(&$store, $idx_path, $pack_path) {
|
|
$buffer = @file_get_contents($idx_path);
|
|
if ($buffer === FALSE || strlen($buffer) < 1068
|
|
|| substr($buffer, 0, 4) !== "\xfftOc"
|
|
|| git_object_store_uint32($buffer, 4) !== 2) {
|
|
return FALSE;
|
|
}
|
|
|
|
$count = git_object_store_uint32($buffer, 8 + 255 * 4);
|
|
$oids_offset = 8 + 256 * 4;
|
|
$offsets_offset = $oids_offset + $count * 20 + $count * 4;
|
|
$large_offsets_offset = $offsets_offset + $count * 4;
|
|
if ($large_offsets_offset + 40 > strlen($buffer)) {
|
|
return FALSE;
|
|
}
|
|
|
|
$pack_key = $pack_path;
|
|
if (!isset($store['pack_offsets'][$pack_key])) {
|
|
$store['pack_offsets'][$pack_key] = array();
|
|
}
|
|
|
|
for ($index = 0; $index < $count; $index += 1) {
|
|
$oid = bin2hex(substr($buffer, $oids_offset + $index * 20, 20));
|
|
$offset = git_object_store_uint32($buffer, $offsets_offset + $index * 4);
|
|
if (($offset & 0x80000000) !== 0) {
|
|
$large_index = $offset & 0x7fffffff;
|
|
$position = $large_offsets_offset + $large_index * 8;
|
|
if ($position + 8 > strlen($buffer)) {
|
|
return FALSE;
|
|
}
|
|
$high = git_object_store_uint32($buffer, $position);
|
|
$low = git_object_store_uint32($buffer, $position + 4);
|
|
$offset = $high * 4294967296 + $low;
|
|
}
|
|
|
|
$store['locations'][$oid] = array($pack_path, $offset);
|
|
$store['pack_offsets'][$pack_key][(string) $offset] = $oid;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
function git_object_store_parse_loose($compressed, $expected_oid) {
|
|
$inflated = @zlib_decode($compressed);
|
|
if ($inflated === FALSE) {
|
|
return FALSE;
|
|
}
|
|
|
|
$separator = strpos($inflated, "\0");
|
|
if ($separator === FALSE
|
|
|| !preg_match('~^(commit|tree|blob|tag) ([0-9]+)$~D', substr($inflated, 0, $separator), $matches)) {
|
|
return FALSE;
|
|
}
|
|
|
|
$body = substr($inflated, $separator + 1);
|
|
if (strlen($body) !== (int) $matches[2]
|
|
|| sha1(substr($inflated, 0, $separator + 1).$body) !== $expected_oid) {
|
|
return FALSE;
|
|
}
|
|
|
|
return array('type' => $matches[1], 'body' => $body, 'oid' => $expected_oid);
|
|
}
|
|
|
|
function git_object_store_read(&$store, $oid) {
|
|
$oid = strtolower($oid);
|
|
if (!preg_match('~^[0-9a-f]{40}$~D', $oid)) {
|
|
return FALSE;
|
|
}
|
|
if (isset($store['objects'][$oid])) {
|
|
return $store['objects'][$oid];
|
|
}
|
|
|
|
$loose_path = get_safe_file_path(
|
|
$store['path'],
|
|
'/objects/'.substr($oid, 0, 2).'/'.substr($oid, 2));
|
|
if ($loose_path !== FALSE) {
|
|
$compressed = @file_get_contents($loose_path);
|
|
$object = $compressed === FALSE ? FALSE : git_object_store_parse_loose($compressed, $oid);
|
|
} else if (isset($store['locations'][$oid])) {
|
|
$location = $store['locations'][$oid];
|
|
$object = git_object_store_read_pack_offset($store, $location[0], $location[1]);
|
|
if ($object !== FALSE && $object['oid'] !== $oid) {
|
|
$object = FALSE;
|
|
}
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
|
|
if ($object !== FALSE) {
|
|
$store['objects'][$oid] = $object;
|
|
}
|
|
return $object;
|
|
}
|
|
|
|
function git_object_store_read_byte($file) {
|
|
$byte = fread($file, 1);
|
|
return $byte === FALSE || strlen($byte) !== 1 ? FALSE : ord($byte);
|
|
}
|
|
|
|
function git_object_store_read_pack_offset(&$store, $pack_path, $offset) {
|
|
$cache_key = $pack_path.':'.$offset;
|
|
if (isset($store['pack_objects'][$cache_key])) {
|
|
return $store['pack_objects'][$cache_key];
|
|
}
|
|
|
|
$file = @fopen($pack_path, 'rb');
|
|
if ($file === FALSE || fseek($file, $offset) !== 0) {
|
|
if ($file !== FALSE) {
|
|
fclose($file);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
$byte = git_object_store_read_byte($file);
|
|
if ($byte === FALSE) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
|
|
$type_number = ($byte >> 4) & 7;
|
|
$size = $byte & 15;
|
|
$shift = 4;
|
|
while (($byte & 0x80) !== 0) {
|
|
$byte = git_object_store_read_byte($file);
|
|
if ($byte === FALSE || $shift > 60) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
$size |= ($byte & 0x7f) << $shift;
|
|
$shift += 7;
|
|
}
|
|
|
|
$base_offset = NULL;
|
|
$base_oid = NULL;
|
|
if ($type_number === 6) {
|
|
$byte = git_object_store_read_byte($file);
|
|
if ($byte === FALSE) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
$distance = $byte & 0x7f;
|
|
while (($byte & 0x80) !== 0) {
|
|
$byte = git_object_store_read_byte($file);
|
|
if ($byte === FALSE) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
$distance = (($distance + 1) << 7) | ($byte & 0x7f);
|
|
}
|
|
$base_offset = $offset - $distance;
|
|
if ($base_offset < 12) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
} else if ($type_number === 7) {
|
|
$binary_oid = fread($file, 20);
|
|
if ($binary_oid === FALSE || strlen($binary_oid) !== 20) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
$base_oid = bin2hex($binary_oid);
|
|
}
|
|
|
|
$context = @inflate_init(ZLIB_ENCODING_DEFLATE);
|
|
if ($context === FALSE) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
|
|
$body = '';
|
|
while (inflate_get_status($context) !== ZLIB_STREAM_END && !feof($file)) {
|
|
$chunk = fread($file, 65536);
|
|
if ($chunk === FALSE || $chunk === '') {
|
|
break;
|
|
}
|
|
$inflated = @inflate_add($context, $chunk, ZLIB_SYNC_FLUSH);
|
|
if ($inflated === FALSE) {
|
|
fclose($file);
|
|
return FALSE;
|
|
}
|
|
$body .= $inflated;
|
|
}
|
|
fclose($file);
|
|
|
|
if (inflate_get_status($context) !== ZLIB_STREAM_END || strlen($body) !== $size) {
|
|
return FALSE;
|
|
}
|
|
|
|
$types = array(1 => 'commit', 2 => 'tree', 3 => 'blob', 4 => 'tag');
|
|
if (isset($types[$type_number])) {
|
|
$type = $types[$type_number];
|
|
} else if ($type_number === 6 || $type_number === 7) {
|
|
$base = $type_number === 6
|
|
? git_object_store_read_pack_offset($store, $pack_path, $base_offset)
|
|
: git_object_store_read($store, $base_oid);
|
|
if ($base === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$body = git_object_store_apply_delta($base['body'], $body);
|
|
if ($body === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$type = $base['type'];
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
|
|
$oid = sha1($type.' '.strlen($body)."\0".$body);
|
|
$object = array('type' => $type, 'body' => $body, 'oid' => $oid);
|
|
$store['pack_objects'][$cache_key] = $object;
|
|
$store['objects'][$oid] = $object;
|
|
return $object;
|
|
}
|
|
|
|
function git_object_store_delta_varint($delta, &$position) {
|
|
$value = 0;
|
|
$shift = 0;
|
|
do {
|
|
if ($position >= strlen($delta) || $shift > 56) {
|
|
return FALSE;
|
|
}
|
|
$byte = ord($delta[$position]);
|
|
$position += 1;
|
|
$value |= ($byte & 0x7f) << $shift;
|
|
$shift += 7;
|
|
} while (($byte & 0x80) !== 0);
|
|
|
|
return $value;
|
|
}
|
|
|
|
function git_object_store_apply_delta($base, $delta, $max_result_bytes=0) {
|
|
$position = 0;
|
|
$base_size = git_object_store_delta_varint($delta, $position);
|
|
$result_size = git_object_store_delta_varint($delta, $position);
|
|
if ($base_size === FALSE || $result_size === FALSE || $base_size !== strlen($base)
|
|
|| ($max_result_bytes > 0 && $result_size > $max_result_bytes)) {
|
|
return FALSE;
|
|
}
|
|
|
|
$result = '';
|
|
while ($position < strlen($delta)) {
|
|
$command = ord($delta[$position]);
|
|
$position += 1;
|
|
if (($command & 0x80) === 0) {
|
|
if ($command === 0 || $position + $command > strlen($delta)) {
|
|
return FALSE;
|
|
}
|
|
$result .= substr($delta, $position, $command);
|
|
$position += $command;
|
|
continue;
|
|
}
|
|
|
|
$copy_offset = 0;
|
|
$copy_size = 0;
|
|
$offset_shift = 0;
|
|
for ($mask = 1; $mask <= 8; $mask <<= 1) {
|
|
if (($command & $mask) !== 0) {
|
|
if ($position >= strlen($delta)) {
|
|
return FALSE;
|
|
}
|
|
$copy_offset |= ord($delta[$position]) << $offset_shift;
|
|
$position += 1;
|
|
}
|
|
$offset_shift += 8;
|
|
}
|
|
$size_shift = 0;
|
|
for ($mask = 16; $mask <= 64; $mask <<= 1) {
|
|
if (($command & $mask) !== 0) {
|
|
if ($position >= strlen($delta)) {
|
|
return FALSE;
|
|
}
|
|
$copy_size |= ord($delta[$position]) << $size_shift;
|
|
$position += 1;
|
|
}
|
|
$size_shift += 8;
|
|
}
|
|
if ($copy_size === 0) {
|
|
$copy_size = 65536;
|
|
}
|
|
if ($copy_offset + $copy_size > strlen($base)) {
|
|
return FALSE;
|
|
}
|
|
$result .= substr($base, $copy_offset, $copy_size);
|
|
}
|
|
|
|
return strlen($result) === $result_size ? $result : FALSE;
|
|
}
|
|
|
|
function git_object_store_links($object) {
|
|
$links = array();
|
|
if ($object['type'] === 'commit') {
|
|
$headers = strstr($object['body'], "\n\n", TRUE);
|
|
if ($headers === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$lines = explode("\n", $headers);
|
|
if (empty($lines) || !preg_match('~^tree ([0-9a-f]{40})$~D', $lines[0], $matches)) {
|
|
return FALSE;
|
|
}
|
|
$links[] = $matches[1];
|
|
foreach (array_slice($lines, 1) as $line) {
|
|
if (strpos($line, 'parent ') === 0) {
|
|
if (!preg_match('~^parent ([0-9a-f]{40})$~D', $line, $matches)) {
|
|
return FALSE;
|
|
}
|
|
$links[] = $matches[1];
|
|
}
|
|
}
|
|
} else if ($object['type'] === 'tag') {
|
|
$headers = strstr($object['body'], "\n\n", TRUE);
|
|
if ($headers === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$lines = explode("\n", $headers);
|
|
if (count($lines) < 3
|
|
|| !preg_match('~^object ([0-9a-f]{40})$~D', $lines[0], $object_match)
|
|
|| !preg_match('~^type (commit|tree|blob|tag)$~D', $lines[1])
|
|
|| !preg_match('~^tag .+$~D', $lines[2])) {
|
|
return FALSE;
|
|
}
|
|
$links[] = $object_match[1];
|
|
} else if ($object['type'] === 'tree') {
|
|
$position = 0;
|
|
$length = strlen($object['body']);
|
|
while ($position < $length) {
|
|
$separator = strpos($object['body'], "\0", $position);
|
|
if ($separator === FALSE || $separator + 21 > $length
|
|
|| !preg_match(
|
|
'~^(?:40000|100644|100755|120000|160000) [^/\x00]+$~D',
|
|
substr($object['body'], $position, $separator - $position))) {
|
|
return FALSE;
|
|
}
|
|
$links[] = bin2hex(substr($object['body'], $separator + 1, 20));
|
|
$position = $separator + 21;
|
|
}
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
function git_object_store_validate_links(&$store, $object, $links) {
|
|
if ($object['type'] === 'commit') {
|
|
foreach ($links as $index => $oid) {
|
|
$linked = git_object_store_read($store, $oid);
|
|
if ($linked === FALSE
|
|
|| ($index === 0 && $linked['type'] !== 'tree')
|
|
|| ($index > 0 && $linked['type'] !== 'commit')) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
} else if ($object['type'] === 'tag') {
|
|
if (!preg_match('~^type (commit|tree|blob|tag)$~m', $object['body'], $matches)) {
|
|
return FALSE;
|
|
}
|
|
$linked = git_object_store_read($store, $links[0]);
|
|
if ($linked === FALSE || $linked['type'] !== $matches[1]) {
|
|
return FALSE;
|
|
}
|
|
} else if ($object['type'] === 'tree') {
|
|
foreach ($links as $oid) {
|
|
if (git_object_store_read($store, $oid) === FALSE) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
function git_object_store_collect(&$store, $roots) {
|
|
$objects = array();
|
|
$pending = array_values($roots);
|
|
while (!empty($pending)) {
|
|
$oid = array_pop($pending);
|
|
if (isset($objects[$oid])) {
|
|
continue;
|
|
}
|
|
$object = git_object_store_read($store, $oid);
|
|
if ($object === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$links = git_object_store_links($object);
|
|
if ($links === FALSE || !git_object_store_validate_links($store, $object, $links)) {
|
|
return FALSE;
|
|
}
|
|
$objects[$oid] = $object;
|
|
foreach ($links as $link) {
|
|
if (!isset($objects[$link])) {
|
|
$pending[] = $link;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $objects;
|
|
}
|
|
|
|
function git_object_store_pack_header($type, $size) {
|
|
$types = array('commit' => 1, 'tree' => 2, 'blob' => 3, 'tag' => 4);
|
|
if (!isset($types[$type])) {
|
|
return FALSE;
|
|
}
|
|
|
|
$byte = ($types[$type] << 4) | ($size & 15);
|
|
$size >>= 4;
|
|
$header = '';
|
|
while ($size > 0) {
|
|
$header .= chr($byte | 0x80);
|
|
$byte = $size & 0x7f;
|
|
$size >>= 7;
|
|
}
|
|
return $header.chr($byte);
|
|
}
|
|
|
|
function git_object_store_build_pack($objects) {
|
|
$pack = 'PACK'.pack('N', 2).pack('N', count($objects));
|
|
foreach ($objects as $object) {
|
|
$header = git_object_store_pack_header($object['type'], strlen($object['body']));
|
|
$compressed = gzcompress($object['body']);
|
|
if ($header === FALSE || $compressed === FALSE) {
|
|
return FALSE;
|
|
}
|
|
$pack .= $header.$compressed;
|
|
}
|
|
|
|
return $pack.hash('sha1', $pack, TRUE);
|
|
} |