1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.lfs;
11
12 import static java.nio.charset.StandardCharsets.UTF_8;
13
14 import java.io.BufferedInputStream;
15 import java.io.BufferedReader;
16 import java.io.ByteArrayInputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.io.OutputStream;
21 import java.io.PrintStream;
22 import java.io.UnsupportedEncodingException;
23 import java.util.Locale;
24 import java.util.Objects;
25
26 import org.eclipse.jgit.annotations.Nullable;
27 import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
28 import org.eclipse.jgit.lfs.lib.Constants;
29 import org.eclipse.jgit.lfs.lib.LongObjectId;
30 import org.eclipse.jgit.util.IO;
31
32
33
34
35
36
37 public class LfsPointer implements Comparable<LfsPointer> {
38
39
40
41 public static final String VERSION = "https://git-lfs.github.com/spec/v1"; //$NON-NLS-1$
42
43
44
45
46
47 public static final String VERSION_LEGACY = "https://hawser.github.com/spec/v1"; //$NON-NLS-1$
48
49
50
51
52
53
54 public static final int SIZE_THRESHOLD = 200;
55
56
57
58
59
60 public static final String HASH_FUNCTION_NAME = Constants.LONG_HASH_FUNCTION
61 .toLowerCase(Locale.ROOT).replace("-", "");
62
63
64
65
66
67 static final int FULL_SIZE_THRESHOLD = 8 * 1024;
68
69 private final AnyLongObjectId oid;
70
71 private final long size;
72
73
74
75
76
77
78
79
80
81 public LfsPointer(AnyLongObjectId oid, long size) {
82 this.oid = oid;
83 this.size = size;
84 }
85
86
87
88
89
90
91 public AnyLongObjectId getOid() {
92 return oid;
93 }
94
95
96
97
98
99
100 public long getSize() {
101 return size;
102 }
103
104
105
106
107
108
109
110
111 public void encode(OutputStream out) {
112 try (PrintStream ps = new PrintStream(out, false,
113 UTF_8.name())) {
114 ps.print("version ");
115 ps.print(VERSION + "\n");
116 ps.print("oid " + HASH_FUNCTION_NAME + ":");
117 ps.print(oid.name() + "\n");
118 ps.print("size ");
119 ps.print(size + "\n");
120 } catch (UnsupportedEncodingException e) {
121
122 }
123 }
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 @Nullable
141 public static LfsPointer parseLfsPointer(InputStream in)
142 throws IOException {
143 if (in.markSupported()) {
144 return parse(in);
145 }
146
147
148 return parse(new BufferedInputStream(in));
149 }
150
151 @Nullable
152 private static LfsPointer parse(InputStream in)
153 throws IOException {
154 if (!in.markSupported()) {
155
156 throw new IllegalArgumentException(
157 "LFS pointer parsing needs InputStream.markSupported() == true");
158 }
159
160 in.mark(SIZE_THRESHOLD);
161 byte[] preamble = new byte[SIZE_THRESHOLD];
162 int length = IO.readFully(in, preamble, 0);
163 if (length < preamble.length || in.read() < 0) {
164
165 try (BufferedReader r = new BufferedReader(new InputStreamReader(
166 new ByteArrayInputStream(preamble, 0, length), UTF_8))) {
167 LfsPointer ptr = parse(r);
168 if (ptr == null) {
169 in.reset();
170 }
171 return ptr;
172 }
173 }
174
175 boolean hasVersion = checkVersion(preamble);
176 in.reset();
177 if (!hasVersion) {
178 return null;
179 }
180 in.mark(FULL_SIZE_THRESHOLD);
181 byte[] fullPointer = new byte[FULL_SIZE_THRESHOLD];
182 length = IO.readFully(in, fullPointer, 0);
183 if (length == fullPointer.length && in.read() >= 0) {
184 in.reset();
185 return null;
186 }
187 try (BufferedReader r = new BufferedReader(new InputStreamReader(
188 new ByteArrayInputStream(fullPointer, 0, length), UTF_8))) {
189 LfsPointer ptr = parse(r);
190 if (ptr == null) {
191 in.reset();
192 }
193 return ptr;
194 }
195 }
196
197 private static LfsPointer parse(BufferedReader r) throws IOException {
198 boolean versionLine = false;
199 LongObjectId id = null;
200 long sz = -1;
201
202
203
204
205 for (String s = r.readLine(); s != null; s = r.readLine()) {
206 if (s.startsWith("#") || s.length() == 0) {
207 continue;
208 } else if (s.startsWith("version")) {
209 if (versionLine || !checkVersionLine(s)) {
210 return null;
211 }
212 versionLine = true;
213 } else {
214 try {
215 if (s.startsWith("oid sha256:")) {
216 if (id != null) {
217 return null;
218 }
219 id = LongObjectId.fromString(s.substring(11).trim());
220 } else if (s.startsWith("size")) {
221 if (sz > 0 || s.length() < 5 || s.charAt(4) != ' ') {
222 return null;
223 }
224 sz = Long.parseLong(s.substring(5).trim());
225 }
226 } catch (RuntimeException e) {
227
228
229
230 if (versionLine) {
231 throw e;
232 }
233 return null;
234 }
235 }
236 if (versionLine && id != null && sz > -1) {
237 return new LfsPointer(id, sz);
238 }
239 }
240 return null;
241 }
242
243 private static boolean checkVersion(byte[] data) {
244
245
246
247 try (BufferedReader r = new BufferedReader(
248 new InputStreamReader(new ByteArrayInputStream(data), UTF_8))) {
249 String s = r.readLine();
250 if (s != null && s.startsWith("version")) {
251 return checkVersionLine(s);
252 }
253 } catch (IOException e) {
254
255 }
256 return false;
257 }
258
259 private static boolean checkVersionLine(String s) {
260 if (s.length() < 8 || s.charAt(7) != ' ') {
261 return false;
262 }
263 String rest = s.substring(8).trim();
264 return VERSION.equals(rest) || VERSION_LEGACY.equals(rest);
265 }
266
267
268 @Override
269 public String toString() {
270 return "LfsPointer: oid=" + oid.name() + ", size="
271 + size;
272 }
273
274
275
276
277 @Override
278 public int compareTo(LfsPointer o) {
279 int x = getOid().compareTo(o.getOid());
280 if (x != 0) {
281 return x;
282 }
283
284 return Long.compare(getSize(), o.getSize());
285 }
286
287 @Override
288 public int hashCode() {
289 return Objects.hash(getOid()) * 31 + Long.hashCode(getSize());
290 }
291
292 @Override
293 public boolean equals(Object obj) {
294 if (this == obj) {
295 return true;
296 }
297 if (obj == null || getClass() != obj.getClass()) {
298 return false;
299 }
300 LfsPointer other = (LfsPointer) obj;
301 return Objects.equals(getOid(), other.getOid())
302 && getSize() == other.getSize();
303 }
304 }