summaryrefslogtreecommitdiffstats
path: root/3rdparty/granite/git/object/index.php
blob: 239706d4efdd05a7d613fdb0a18165eee1c9acc9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<?php
/**
 * Index - provides an 'index' object for packfile indexes
 *
 * PHP version 5.3
 *
 * @category Git
 * @package  Granite
 * @author   Craig Roberts <craig0990@googlemail.com>
 * @license  http://www.opensource.org/licenses/mit-license.php MIT License
 * @link     http://craig0990.github.com/Granite/
 */

namespace Granite\Git\Object;
use \UnexpectedValueException as UnexpectedValueException;

/**
 * Index represents a packfile index
 *
 * @category Git
 * @package  Granite
 * @author   Craig Roberts <craig0990@googlemail.com>
 * @license  http://www.opensource.org/licenses/mit-license.php MIT License
 * @link     http://craig0990.github.com/Granite/
 */
class Index
{
    const INDEX_MAGIC = "\377tOc";

    /**
     * The full path to the packfile index
     */
    private $path;
    /**
     * The offset at which the fanout begins, version 2+ indexes have a 2-byte header
     */
    private $offset = 8;
    /**
     * The size of the SHA-1 entries, version 1 stores 4-byte offsets alongside to
     * total 24 bytes, version 2+ stores offsets separately
     */
    private $size = 20;
    /**
     * The version of the index file format, versions 1 and 2 are in use and
     * currently supported
     */
    private $version;

    /**
     * Fetches a raw Git object and parses the result
     *
     * @param string $path     The path to the repository root
     * @param string $packname The name of the packfile index to read
     */
    public function __construct($path, $packname)
    {
        $this->path = $path
            . 'objects'
            . DIRECTORY_SEPARATOR
            . 'pack'
            . DIRECTORY_SEPARATOR
            . 'pack-' . $packname . '.idx';

        $this->version = $this->_readVersion();
        if ($this->version !== 1 && $this->version !== 2) {
            throw new UnexpectedValueException(
                "Unsupported index version (version $version)"
            );
        }

        if ($this->version == 1) {
            $this->offset = 0; // Version 1 index has no header/version
            $this->size = 24; // Offsets + SHA-1 ids are stored together
        }
    }

    /**
     * Returns the offset of the object stored in the index
     *
     * @param string $sha The SHA-1 id of the object being requested
     *
     * @return int The offset of the object in the packfile
     */
    public function find($sha)
    {
        $index = fopen($this->path, 'rb');
        $offset = false; // Offset for object in packfile not found by default

        // Read the fanout to skip to the start char in the sorted SHA-1 list
        list($start, $after) = $this->_readFanout($index, $sha);

        if ($start == $after) {
            fclose($index);
            return false; // Object is apparently located in a 0-length section
        }

        // Seek $offset + 255 4-byte fanout entries and read 256th entry
        fseek($index, $this->offset + 4 * 255);
        $totalObjects = $this->_uint32($index);

        // Look up the SHA-1 id of the object
        // TODO: Binary search
        fseek($index, $this->offset + 1024 + $this->size * $start);
        for ($i = $start; $i < $after; $i++) {
            if ($this->version == 1) {
                $offset = $this->_uint32($index);
            }

            $name = fread($index, 20);
            if ($name == pack('H40', $sha)) {
                break; // Found it
            }
        }

        if ($i == $after) {
            fclose($index);
            return false; // Scanned entire section, couldn't find it
        }

        if ($this->version == 2) {
            // Jump to the offset location and read it
            fseek($index, 1032 + 24 * $totalObjects + 4 * $i);
            $offset = $this->_uint32($index);
            if ($offset & 0x80000000) {
                // Offset is a 64-bit integer; packfile is larger than 2GB
                fclose($index);
                throw new UnexpectedValueException(
                    "Packfile larger than 2GB, currently unsupported"
                );
            }
        }

        fclose($index);
        return $offset;
    }

    /**
     * Converts a binary string into a 32-bit unsigned integer
     *
     * @param handle $file Binary string to convert
     *
     * @return int Integer value
     */
    private function _uint32($file)
    {
        $val = unpack('Nx', fread($file, 4));
        return $val['x'];
    }

    /**
     * Reads the fanout for a particular SHA-1 id
     *
     * Largely modified from Glip, with some reference to Grit - largely because I
     * can't see how to re-implement this in PHP
     *
     * @param handle $file   File handle to the index file
     * @param string $sha    The SHA-1 id to search for
     * @param int    $offset The offset at which the fanout begins
     *
     * @return array Array containing integer 'start' and
     *               'past-the-end' locations
     */
    private function _readFanout($file, $sha)
    {
        $sha = pack('H40', $sha);
        fseek($file, $this->offset);
        if ($sha{0} == "\00") {
            /**
             * First character is 0, read first fanout entry to provide
             * 'past-the-end' location (since first fanout entry provides start
             * point for '1'-prefixed SHA-1 ids)
             */
            $start = 0;
            fseek($file, $this->offset); // Jump to start of fanout, $offset bytes in
            $after = $this->_uint32($file);
        } else {
            /**
             * Take ASCII value of first character, minus one to get the fanout
             * position of the offset (minus one because the fanout does not
             * contain an entry for "\00"), multiplied by four bytes per entry
             */
            fseek($file, $this->offset + (ord($sha{0}) - 1) * 4);
            $start = $this->_uint32($file);
            $after = $this->_uint32($file);
        }

        return array($start, $after);
    }

    /**
     * Returns the version number of the index file, or 1 if there is no version
     * information
     *
     * @return int
     */
    private function _readVersion()
    {
        $file = fopen($this->path, 'rb');
        $magic = fread($file, 4);
        $version = $this->_uint32($file);

        if ($magic !== self::INDEX_MAGIC) {
            $version = 1;
        }

        fclose($file);
        return $version;
    }

}