You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LfsPointer.java 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /*
  2. * Copyright (C) 2016, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.lfs;
  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import java.io.BufferedInputStream;
  13. import java.io.BufferedReader;
  14. import java.io.ByteArrayInputStream;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.io.InputStreamReader;
  18. import java.io.OutputStream;
  19. import java.io.PrintStream;
  20. import java.io.UnsupportedEncodingException;
  21. import java.util.Locale;
  22. import java.util.Objects;
  23. import org.eclipse.jgit.annotations.Nullable;
  24. import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
  25. import org.eclipse.jgit.lfs.lib.Constants;
  26. import org.eclipse.jgit.lfs.lib.LongObjectId;
  27. import org.eclipse.jgit.util.IO;
  28. /**
  29. * Represents an LFS pointer file
  30. *
  31. * @since 4.6
  32. */
  33. public class LfsPointer implements Comparable<LfsPointer> {
  34. /**
  35. * The version of the LfsPointer file format
  36. */
  37. public static final String VERSION = "https://git-lfs.github.com/spec/v1"; //$NON-NLS-1$
  38. /**
  39. * The version of the LfsPointer file format using legacy URL
  40. * @since 4.7
  41. */
  42. public static final String VERSION_LEGACY = "https://hawser.github.com/spec/v1"; //$NON-NLS-1$
  43. /**
  44. * Don't inspect files that are larger than this threshold to avoid
  45. * excessive reading. No pointer file should be larger than this.
  46. * @since 4.11
  47. */
  48. public static final int SIZE_THRESHOLD = 200;
  49. /**
  50. * The name of the hash function as used in the pointer files. This will
  51. * evaluate to "sha256"
  52. */
  53. public static final String HASH_FUNCTION_NAME = Constants.LONG_HASH_FUNCTION
  54. .toLowerCase(Locale.ROOT).replace("-", ""); //$NON-NLS-1$ //$NON-NLS-2$
  55. /**
  56. * {@link #SIZE_THRESHOLD} is too low; with lfs extensions a LFS pointer can
  57. * be larger. But 8kB should be more than enough.
  58. */
  59. static final int FULL_SIZE_THRESHOLD = 8 * 1024;
  60. private final AnyLongObjectId oid;
  61. private final long size;
  62. /**
  63. * <p>Constructor for LfsPointer.</p>
  64. *
  65. * @param oid
  66. * the id of the content
  67. * @param size
  68. * the size of the content
  69. */
  70. public LfsPointer(AnyLongObjectId oid, long size) {
  71. this.oid = oid;
  72. this.size = size;
  73. }
  74. /**
  75. * <p>Getter for the field <code>oid</code>.</p>
  76. *
  77. * @return the id of the content
  78. */
  79. public AnyLongObjectId getOid() {
  80. return oid;
  81. }
  82. /**
  83. * <p>Getter for the field <code>size</code>.</p>
  84. *
  85. * @return the size of the content
  86. */
  87. public long getSize() {
  88. return size;
  89. }
  90. /**
  91. * Encode this object into the LFS format defined by {@link #VERSION}
  92. *
  93. * @param out
  94. * the {@link java.io.OutputStream} into which the encoded data should be
  95. * written
  96. */
  97. public void encode(OutputStream out) {
  98. try (PrintStream ps = new PrintStream(out, false,
  99. UTF_8.name())) {
  100. ps.print("version "); //$NON-NLS-1$
  101. ps.print(VERSION + "\n"); //$NON-NLS-1$
  102. ps.print("oid " + HASH_FUNCTION_NAME + ":"); //$NON-NLS-1$ //$NON-NLS-2$
  103. ps.print(oid.name() + "\n"); //$NON-NLS-1$
  104. ps.print("size "); //$NON-NLS-1$
  105. ps.print(size + "\n"); //$NON-NLS-1$
  106. } catch (UnsupportedEncodingException e) {
  107. // should not happen, we are using a standard charset
  108. }
  109. }
  110. /**
  111. * Try to parse the data provided by an InputStream to the format defined by
  112. * {@link #VERSION}. If the given stream supports mark and reset as
  113. * indicated by {@link InputStream#markSupported()}, its input position will
  114. * be reset if the stream content is not actually a LFS pointer (i.e., when
  115. * {@code null} is returned). If the stream content is an invalid LFS
  116. * pointer or the given stream does not support mark/reset, the input
  117. * position may not be reset.
  118. *
  119. * @param in
  120. * the {@link java.io.InputStream} from where to read the data
  121. * @return an {@link org.eclipse.jgit.lfs.LfsPointer} or {@code null} if the
  122. * stream was not parseable as LfsPointer
  123. * @throws java.io.IOException
  124. */
  125. @Nullable
  126. public static LfsPointer parseLfsPointer(InputStream in)
  127. throws IOException {
  128. if (in.markSupported()) {
  129. return parse(in);
  130. }
  131. // Fallback; note that while parse() resets its input stream, that won't
  132. // reset "in".
  133. return parse(new BufferedInputStream(in));
  134. }
  135. @Nullable
  136. private static LfsPointer parse(InputStream in)
  137. throws IOException {
  138. if (!in.markSupported()) {
  139. // No translation; internal error
  140. throw new IllegalArgumentException(
  141. "LFS pointer parsing needs InputStream.markSupported() == true"); //$NON-NLS-1$
  142. }
  143. // Try reading only a short block first.
  144. in.mark(SIZE_THRESHOLD);
  145. byte[] preamble = new byte[SIZE_THRESHOLD];
  146. int length = IO.readFully(in, preamble, 0);
  147. if (length < preamble.length || in.read() < 0) {
  148. // We have the whole file. Try to parse a pointer from it.
  149. try (BufferedReader r = new BufferedReader(new InputStreamReader(
  150. new ByteArrayInputStream(preamble, 0, length), UTF_8))) {
  151. LfsPointer ptr = parse(r);
  152. if (ptr == null) {
  153. in.reset();
  154. }
  155. return ptr;
  156. }
  157. }
  158. // Longer than SIZE_THRESHOLD: expect "version" to be the first line.
  159. boolean hasVersion = checkVersion(preamble);
  160. in.reset();
  161. if (!hasVersion) {
  162. return null;
  163. }
  164. in.mark(FULL_SIZE_THRESHOLD);
  165. byte[] fullPointer = new byte[FULL_SIZE_THRESHOLD];
  166. length = IO.readFully(in, fullPointer, 0);
  167. if (length == fullPointer.length && in.read() >= 0) {
  168. in.reset();
  169. return null; // Too long.
  170. }
  171. try (BufferedReader r = new BufferedReader(new InputStreamReader(
  172. new ByteArrayInputStream(fullPointer, 0, length), UTF_8))) {
  173. LfsPointer ptr = parse(r);
  174. if (ptr == null) {
  175. in.reset();
  176. }
  177. return ptr;
  178. }
  179. }
  180. private static LfsPointer parse(BufferedReader r) throws IOException {
  181. boolean versionLine = false;
  182. LongObjectId id = null;
  183. long sz = -1;
  184. // This parsing is a bit too general if we go by the spec at
  185. // https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
  186. // Comment lines are not mentioned in the spec, the "version" line
  187. // MUST be the first, and keys are ordered alphabetically.
  188. for (String s = r.readLine(); s != null; s = r.readLine()) {
  189. if (s.startsWith("#") || s.length() == 0) { //$NON-NLS-1$
  190. continue;
  191. } else if (s.startsWith("version")) { //$NON-NLS-1$
  192. if (versionLine || !checkVersionLine(s)) {
  193. return null; // Not a LFS pointer
  194. }
  195. versionLine = true;
  196. } else {
  197. try {
  198. if (s.startsWith("oid sha256:")) { //$NON-NLS-1$
  199. if (id != null) {
  200. return null; // Not a LFS pointer
  201. }
  202. id = LongObjectId.fromString(s.substring(11).trim());
  203. } else if (s.startsWith("size")) { //$NON-NLS-1$
  204. if (sz > 0 || s.length() < 5 || s.charAt(4) != ' ') {
  205. return null; // Not a LFS pointer
  206. }
  207. sz = Long.parseLong(s.substring(5).trim());
  208. }
  209. } catch (RuntimeException e) {
  210. // We could not parse the line. If we have a version
  211. // already, this is a corrupt LFS pointer. Otherwise it
  212. // is just not an LFS pointer.
  213. if (versionLine) {
  214. throw e;
  215. }
  216. return null;
  217. }
  218. }
  219. if (versionLine && id != null && sz > -1) {
  220. return new LfsPointer(id, sz);
  221. }
  222. }
  223. return null;
  224. }
  225. private static boolean checkVersion(byte[] data) {
  226. // According to the spec at
  227. // https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
  228. // it MUST always be the first line.
  229. try (BufferedReader r = new BufferedReader(
  230. new InputStreamReader(new ByteArrayInputStream(data), UTF_8))) {
  231. String s = r.readLine();
  232. if (s != null && s.startsWith("version")) { //$NON-NLS-1$
  233. return checkVersionLine(s);
  234. }
  235. } catch (IOException e) {
  236. // Doesn't occur, we're reading from a byte array!
  237. }
  238. return false;
  239. }
  240. private static boolean checkVersionLine(String s) {
  241. if (s.length() < 8 || s.charAt(7) != ' ') {
  242. return false; // Not a valid LFS pointer version line
  243. }
  244. String rest = s.substring(8).trim();
  245. return VERSION.equals(rest) || VERSION_LEGACY.equals(rest);
  246. }
  247. /** {@inheritDoc} */
  248. @Override
  249. public String toString() {
  250. return "LfsPointer: oid=" + oid.name() + ", size=" //$NON-NLS-1$ //$NON-NLS-2$
  251. + size;
  252. }
  253. /**
  254. * @since 4.11
  255. */
  256. @Override
  257. public int compareTo(LfsPointer o) {
  258. int x = getOid().compareTo(o.getOid());
  259. if (x != 0) {
  260. return x;
  261. }
  262. return Long.compare(getSize(), o.getSize());
  263. }
  264. @Override
  265. public int hashCode() {
  266. return Objects.hash(getOid()) * 31 + Long.hashCode(getSize());
  267. }
  268. @Override
  269. public boolean equals(Object obj) {
  270. if (this == obj) {
  271. return true;
  272. }
  273. if (obj == null || getClass() != obj.getClass()) {
  274. return false;
  275. }
  276. LfsPointer other = (LfsPointer) obj;
  277. return Objects.equals(getOid(), other.getOid())
  278. && getSize() == other.getSize();
  279. }
  280. }