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 package org.eclipse.jgit.lib;
46
47 import static org.eclipse.jgit.util.RawParseUtils.match;
48 import static org.eclipse.jgit.util.RawParseUtils.nextLF;
49 import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
50
51 import java.lang.reflect.InvocationTargetException;
52 import java.lang.reflect.Method;
53 import java.text.MessageFormat;
54 import java.util.HashSet;
55 import java.util.Locale;
56 import java.util.Set;
57
58 import org.eclipse.jgit.errors.CorruptObjectException;
59 import org.eclipse.jgit.internal.JGitText;
60 import org.eclipse.jgit.util.MutableInteger;
61 import org.eclipse.jgit.util.RawParseUtils;
62
63
64
65
66
67
68
69
70
71
72
73
74 public class ObjectChecker {
75
76 public static final byte[] tree = Constants.encodeASCII("tree ");
77
78
79 public static final byte[] parent = Constants.encodeASCII("parent ");
80
81
82 public static final byte[] author = Constants.encodeASCII("author ");
83
84
85 public static final byte[] committer = Constants.encodeASCII("committer ");
86
87
88 public static final byte[] encoding = Constants.encodeASCII("encoding ");
89
90
91 public static final byte[] object = Constants.encodeASCII("object ");
92
93
94 public static final byte[] type = Constants.encodeASCII("type ");
95
96
97 public static final byte[] tag = Constants.encodeASCII("tag ");
98
99
100 public static final byte[] tagger = Constants.encodeASCII("tagger ");
101
102 private final MutableObjectId tempId = new MutableObjectId();
103
104 private final MutableInteger ptrout = new MutableInteger();
105
106 private boolean allowZeroMode;
107
108 private boolean allowInvalidPersonIdent;
109 private boolean windows;
110 private boolean macosx;
111
112
113
114
115
116
117
118
119
120
121
122
123
124 public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) {
125 allowZeroMode = allow;
126 return this;
127 }
128
129
130
131
132
133
134
135
136
137
138
139
140 public ObjectChecker setAllowInvalidPersonIdent(boolean allow) {
141 allowInvalidPersonIdent = allow;
142 return this;
143 }
144
145
146
147
148
149
150
151
152
153
154 public ObjectChecker setSafeForWindows(boolean win) {
155 windows = win;
156 return this;
157 }
158
159
160
161
162
163
164
165
166
167
168
169 public ObjectChecker setSafeForMacOS(boolean mac) {
170 macosx = mac;
171 return this;
172 }
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187 public void check(final int objType, final byte[] raw)
188 throws CorruptObjectException {
189 switch (objType) {
190 case Constants.OBJ_COMMIT:
191 checkCommit(raw);
192 break;
193 case Constants.OBJ_TAG:
194 checkTag(raw);
195 break;
196 case Constants.OBJ_TREE:
197 checkTree(raw);
198 break;
199 case Constants.OBJ_BLOB:
200 checkBlob(raw);
201 break;
202 default:
203 throw new CorruptObjectException(MessageFormat.format(
204 JGitText.get().corruptObjectInvalidType2,
205 Integer.valueOf(objType)));
206 }
207 }
208
209 private int id(final byte[] raw, final int ptr) {
210 try {
211 tempId.fromString(raw, ptr);
212 return ptr + Constants.OBJECT_ID_STRING_LENGTH;
213 } catch (IllegalArgumentException e) {
214 return -1;
215 }
216 }
217
218 private int personIdent(final byte[] raw, int ptr) {
219 if (allowInvalidPersonIdent)
220 return nextLF(raw, ptr) - 1;
221
222 final int emailB = nextLF(raw, ptr, '<');
223 if (emailB == ptr || raw[emailB - 1] != '<')
224 return -1;
225
226 final int emailE = nextLF(raw, emailB, '>');
227 if (emailE == emailB || raw[emailE - 1] != '>')
228 return -1;
229 if (emailE == raw.length || raw[emailE] != ' ')
230 return -1;
231
232 parseBase10(raw, emailE + 1, ptrout);
233 ptr = ptrout.value;
234 if (emailE + 1 == ptr)
235 return -1;
236 if (ptr == raw.length || raw[ptr] != ' ')
237 return -1;
238
239 parseBase10(raw, ptr + 1, ptrout);
240 if (ptr + 1 == ptrout.value)
241 return -1;
242 return ptrout.value;
243 }
244
245
246
247
248
249
250
251
252
253 public void checkCommit(final byte[] raw) throws CorruptObjectException {
254 int ptr = 0;
255
256 if ((ptr = match(raw, ptr, tree)) < 0)
257 throw new CorruptObjectException(
258 JGitText.get().corruptObjectNotreeHeader);
259 if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
260 throw new CorruptObjectException(
261 JGitText.get().corruptObjectInvalidTree);
262
263 while (match(raw, ptr, parent) >= 0) {
264 ptr += parent.length;
265 if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
266 throw new CorruptObjectException(
267 JGitText.get().corruptObjectInvalidParent);
268 }
269
270 if ((ptr = match(raw, ptr, author)) < 0)
271 throw new CorruptObjectException(
272 JGitText.get().corruptObjectNoAuthor);
273 if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
274 throw new CorruptObjectException(
275 JGitText.get().corruptObjectInvalidAuthor);
276
277 if ((ptr = match(raw, ptr, committer)) < 0)
278 throw new CorruptObjectException(
279 JGitText.get().corruptObjectNoCommitter);
280 if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
281 throw new CorruptObjectException(
282 JGitText.get().corruptObjectInvalidCommitter);
283 }
284
285
286
287
288
289
290
291
292
293 public void checkTag(final byte[] raw) throws CorruptObjectException {
294 int ptr = 0;
295
296 if ((ptr = match(raw, ptr, object)) < 0)
297 throw new CorruptObjectException(
298 JGitText.get().corruptObjectNoObjectHeader);
299 if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
300 throw new CorruptObjectException(
301 JGitText.get().corruptObjectInvalidObject);
302
303 if ((ptr = match(raw, ptr, type)) < 0)
304 throw new CorruptObjectException(
305 JGitText.get().corruptObjectNoTypeHeader);
306 ptr = nextLF(raw, ptr);
307
308 if ((ptr = match(raw, ptr, tag)) < 0)
309 throw new CorruptObjectException(
310 JGitText.get().corruptObjectNoTagHeader);
311 ptr = nextLF(raw, ptr);
312
313 if ((ptr = match(raw, ptr, tagger)) > 0) {
314 if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
315 throw new CorruptObjectException(
316 JGitText.get().corruptObjectInvalidTagger);
317 }
318 }
319
320 private static int lastPathChar(final int mode) {
321 return FileMode.TREE.equals(mode) ? '/' : '\0';
322 }
323
324 private static int pathCompare(final byte[] raw, int aPos, final int aEnd,
325 final int aMode, int bPos, final int bEnd, final int bMode) {
326 while (aPos < aEnd && bPos < bEnd) {
327 final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff);
328 if (cmp != 0)
329 return cmp;
330 }
331
332 if (aPos < aEnd)
333 return (raw[aPos] & 0xff) - lastPathChar(bMode);
334 if (bPos < bEnd)
335 return lastPathChar(aMode) - (raw[bPos] & 0xff);
336 return 0;
337 }
338
339 private static boolean duplicateName(final byte[] raw,
340 final int thisNamePos, final int thisNameEnd) {
341 final int sz = raw.length;
342 int nextPtr = thisNameEnd + 1 + Constants.OBJECT_ID_LENGTH;
343 for (;;) {
344 int nextMode = 0;
345 for (;;) {
346 if (nextPtr >= sz)
347 return false;
348 final byte c = raw[nextPtr++];
349 if (' ' == c)
350 break;
351 nextMode <<= 3;
352 nextMode += c - '0';
353 }
354
355 final int nextNamePos = nextPtr;
356 for (;;) {
357 if (nextPtr == sz)
358 return false;
359 final byte c = raw[nextPtr++];
360 if (c == 0)
361 break;
362 }
363 if (nextNamePos + 1 == nextPtr)
364 return false;
365
366 final int cmp = pathCompare(raw, thisNamePos, thisNameEnd,
367 FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode);
368 if (cmp < 0)
369 return false;
370 else if (cmp == 0)
371 return true;
372
373 nextPtr += Constants.OBJECT_ID_LENGTH;
374 }
375 }
376
377
378
379
380
381
382
383
384
385 public void checkTree(final byte[] raw) throws CorruptObjectException {
386 final int sz = raw.length;
387 int ptr = 0;
388 int lastNameB = 0, lastNameE = 0, lastMode = 0;
389 Set<String> normalized = windows || macosx
390 ? new HashSet<String>()
391 : null;
392
393 while (ptr < sz) {
394 int thisMode = 0;
395 for (;;) {
396 if (ptr == sz)
397 throw new CorruptObjectException(
398 JGitText.get().corruptObjectTruncatedInMode);
399 final byte c = raw[ptr++];
400 if (' ' == c)
401 break;
402 if (c < '0' || c > '7')
403 throw new CorruptObjectException(
404 JGitText.get().corruptObjectInvalidModeChar);
405 if (thisMode == 0 && c == '0' && !allowZeroMode)
406 throw new CorruptObjectException(
407 JGitText.get().corruptObjectInvalidModeStartsZero);
408 thisMode <<= 3;
409 thisMode += c - '0';
410 }
411
412 if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD)
413 throw new CorruptObjectException(MessageFormat.format(
414 JGitText.get().corruptObjectInvalidMode2,
415 Integer.valueOf(thisMode)));
416
417 final int thisNameB = ptr;
418 ptr = scanPathSegment(raw, ptr, sz);
419 if (ptr == sz || raw[ptr] != 0)
420 throw new CorruptObjectException(
421 JGitText.get().corruptObjectTruncatedInName);
422 checkPathSegment2(raw, thisNameB, ptr);
423 if (normalized != null) {
424 if (!normalized.add(normalize(raw, thisNameB, ptr)))
425 throw new CorruptObjectException(
426 JGitText.get().corruptObjectDuplicateEntryNames);
427 } else if (duplicateName(raw, thisNameB, ptr))
428 throw new CorruptObjectException(
429 JGitText.get().corruptObjectDuplicateEntryNames);
430
431 if (lastNameB != 0) {
432 final int cmp = pathCompare(raw, lastNameB, lastNameE,
433 lastMode, thisNameB, ptr, thisMode);
434 if (cmp > 0)
435 throw new CorruptObjectException(
436 JGitText.get().corruptObjectIncorrectSorting);
437 }
438
439 lastNameB = thisNameB;
440 lastNameE = ptr;
441 lastMode = thisMode;
442
443 ptr += 1 + Constants.OBJECT_ID_LENGTH;
444 if (ptr > sz)
445 throw new CorruptObjectException(
446 JGitText.get().corruptObjectTruncatedInObjectId);
447 }
448 }
449
450 private int scanPathSegment(byte[] raw, int ptr, int end)
451 throws CorruptObjectException {
452 for (; ptr < end; ptr++) {
453 byte c = raw[ptr];
454 if (c == 0)
455 return ptr;
456 if (c == '/')
457 throw new CorruptObjectException(
458 JGitText.get().corruptObjectNameContainsSlash);
459 if (windows && isInvalidOnWindows(c)) {
460 if (c > 31)
461 throw new CorruptObjectException(String.format(
462 JGitText.get().corruptObjectNameContainsChar,
463 Byte.valueOf(c)));
464 throw new CorruptObjectException(String.format(
465 JGitText.get().corruptObjectNameContainsByte,
466 Integer.valueOf(c & 0xff)));
467 }
468 }
469 return ptr;
470 }
471
472
473
474
475
476
477
478
479
480
481
482 public void checkPath(String path) throws CorruptObjectException {
483 byte[] buf = Constants.encode(path);
484 checkPath(buf, 0, buf.length);
485 }
486
487
488
489
490
491
492
493
494
495
496
497
498
499 public void checkPath(byte[] raw, int ptr, int end)
500 throws CorruptObjectException {
501 int start = ptr;
502 for (; ptr < end; ptr++) {
503 if (raw[ptr] == '/') {
504 checkPathSegment(raw, start, ptr);
505 start = ptr + 1;
506 }
507 }
508 checkPathSegment(raw, start, end);
509 }
510
511
512
513
514
515
516
517
518
519
520 public void checkPathSegment(byte[] raw, int ptr, int end)
521 throws CorruptObjectException {
522 int e = scanPathSegment(raw, ptr, end);
523 if (e < end && raw[e] == 0)
524 throw new CorruptObjectException(
525 JGitText.get().corruptObjectNameContainsNullByte);
526 checkPathSegment2(raw, ptr, end);
527 }
528
529 private void checkPathSegment2(byte[] raw, int ptr, int end)
530 throws CorruptObjectException {
531 if (ptr == end)
532 throw new CorruptObjectException(
533 JGitText.get().corruptObjectNameZeroLength);
534 if (raw[ptr] == '.') {
535 switch (end - ptr) {
536 case 1:
537 throw new CorruptObjectException(
538 JGitText.get().corruptObjectNameDot);
539 case 2:
540 if (raw[ptr + 1] == '.')
541 throw new CorruptObjectException(
542 JGitText.get().corruptObjectNameDotDot);
543 break;
544 case 4:
545 if (isGit(raw, ptr + 1))
546 throw new CorruptObjectException(String.format(
547 JGitText.get().corruptObjectInvalidName,
548 RawParseUtils.decode(raw, ptr, end)));
549 break;
550 default:
551 if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end))
552 throw new CorruptObjectException(String.format(
553 JGitText.get().corruptObjectInvalidName,
554 RawParseUtils.decode(raw, ptr, end)));
555 }
556 } else if (isGitTilde1(raw, ptr, end)) {
557 throw new CorruptObjectException(String.format(
558 JGitText.get().corruptObjectInvalidName,
559 RawParseUtils.decode(raw, ptr, end)));
560 }
561
562 if (macosx && isMacHFSGit(raw, ptr, end))
563 throw new CorruptObjectException(String.format(
564 JGitText.get().corruptObjectInvalidNameIgnorableUnicode,
565 RawParseUtils.decode(raw, ptr, end)));
566
567 if (windows) {
568
569 if (raw[end - 1] == ' ' || raw[end - 1] == '.')
570 throw new CorruptObjectException(String.format(
571 JGitText.get().corruptObjectInvalidNameEnd,
572 Character.valueOf(((char) raw[end - 1]))));
573 if (end - ptr >= 3)
574 checkNotWindowsDevice(raw, ptr, end);
575 }
576 }
577
578
579
580 private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
581 throws CorruptObjectException {
582 boolean ignorable = false;
583 byte[] git = new byte[] { '.', 'g', 'i', 't' };
584 int g = 0;
585 while (ptr < end) {
586 switch (raw[ptr]) {
587 case (byte) 0xe2:
588 checkTruncatedIgnorableUTF8(raw, ptr, end);
589 switch (raw[ptr + 1]) {
590 case (byte) 0x80:
591 switch (raw[ptr + 2]) {
592 case (byte) 0x8c:
593 case (byte) 0x8d:
594 case (byte) 0x8e:
595 case (byte) 0x8f:
596 case (byte) 0xaa:
597 case (byte) 0xab:
598 case (byte) 0xac:
599 case (byte) 0xad:
600 case (byte) 0xae:
601 ignorable = true;
602 ptr += 3;
603 continue;
604 default:
605 return false;
606 }
607 case (byte) 0x81:
608 switch (raw[ptr + 2]) {
609 case (byte) 0xaa:
610 case (byte) 0xab:
611 case (byte) 0xac:
612 case (byte) 0xad:
613 case (byte) 0xae:
614 case (byte) 0xaf:
615 ignorable = true;
616 ptr += 3;
617 continue;
618 default:
619 return false;
620 }
621 }
622 break;
623 case (byte) 0xef:
624 checkTruncatedIgnorableUTF8(raw, ptr, end);
625
626 if ((raw[ptr + 1] == (byte) 0xbb)
627 && (raw[ptr + 2] == (byte) 0xbf)) {
628 ignorable = true;
629 ptr += 3;
630 continue;
631 }
632 return false;
633 default:
634 if (g == 4)
635 return false;
636 if (raw[ptr++] != git[g++])
637 return false;
638 }
639 }
640 if (g == 4 && ignorable)
641 return true;
642 return false;
643 }
644
645 private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end)
646 throws CorruptObjectException {
647 if ((ptr + 2) >= end)
648 throw new CorruptObjectException(MessageFormat.format(
649 JGitText.get().corruptObjectInvalidNameInvalidUtf8,
650 toHexString(raw, ptr, end)));
651 }
652
653 private static String toHexString(byte[] raw, int ptr, int end) {
654 StringBuilder b = new StringBuilder("0x");
655 for (int i = ptr; i < end; i++)
656 b.append(String.format("%02x", Byte.valueOf(raw[i])));
657 return b.toString();
658 }
659
660 private static void checkNotWindowsDevice(byte[] raw, int ptr, int end)
661 throws CorruptObjectException {
662 switch (toLower(raw[ptr])) {
663 case 'a':
664 if (end - ptr >= 3
665 && toLower(raw[ptr + 1]) == 'u'
666 && toLower(raw[ptr + 2]) == 'x'
667 && (end - ptr == 3 || raw[ptr + 3] == '.'))
668 throw new CorruptObjectException(
669 JGitText.get().corruptObjectInvalidNameAux);
670 break;
671
672 case 'c':
673 if (end - ptr >= 3
674 && toLower(raw[ptr + 2]) == 'n'
675 && toLower(raw[ptr + 1]) == 'o'
676 && (end - ptr == 3 || raw[ptr + 3] == '.'))
677 throw new CorruptObjectException(
678 JGitText.get().corruptObjectInvalidNameCon);
679 if (end - ptr >= 4
680 && toLower(raw[ptr + 2]) == 'm'
681 && toLower(raw[ptr + 1]) == 'o'
682 && isPositiveDigit(raw[ptr + 3])
683 && (end - ptr == 4 || raw[ptr + 4] == '.'))
684 throw new CorruptObjectException(String.format(
685 JGitText.get().corruptObjectInvalidNameCom,
686 Character.valueOf(((char) raw[ptr + 3]))));
687 break;
688
689 case 'l':
690 if (end - ptr >= 4
691 && toLower(raw[ptr + 1]) == 'p'
692 && toLower(raw[ptr + 2]) == 't'
693 && isPositiveDigit(raw[ptr + 3])
694 && (end - ptr == 4 || raw[ptr + 4] == '.'))
695 throw new CorruptObjectException(String.format(
696 JGitText.get().corruptObjectInvalidNameLpt,
697 Character.valueOf(((char) raw[ptr + 3]))));
698 break;
699
700 case 'n':
701 if (end - ptr >= 3
702 && toLower(raw[ptr + 1]) == 'u'
703 && toLower(raw[ptr + 2]) == 'l'
704 && (end - ptr == 3 || raw[ptr + 3] == '.'))
705 throw new CorruptObjectException(
706 JGitText.get().corruptObjectInvalidNameNul);
707 break;
708
709 case 'p':
710 if (end - ptr >= 3
711 && toLower(raw[ptr + 1]) == 'r'
712 && toLower(raw[ptr + 2]) == 'n'
713 && (end - ptr == 3 || raw[ptr + 3] == '.'))
714 throw new CorruptObjectException(
715 JGitText.get().corruptObjectInvalidNamePrn);
716 break;
717 }
718 }
719
720 private static boolean isInvalidOnWindows(byte c) {
721
722 switch (c) {
723 case '"':
724 case '*':
725 case ':':
726 case '<':
727 case '>':
728 case '?':
729 case '\\':
730 case '|':
731 return true;
732 }
733 return 1 <= c && c <= 31;
734 }
735
736 private static boolean isGit(byte[] buf, int p) {
737 return toLower(buf[p]) == 'g'
738 && toLower(buf[p + 1]) == 'i'
739 && toLower(buf[p + 2]) == 't';
740 }
741
742 private static boolean isGitTilde1(byte[] buf, int p, int end) {
743 if (end - p != 5)
744 return false;
745 return toLower(buf[p]) == 'g' && toLower(buf[p + 1]) == 'i'
746 && toLower(buf[p + 2]) == 't' && buf[p + 3] == '~'
747 && buf[p + 4] == '1';
748 }
749
750 private static boolean isNormalizedGit(byte[] raw, int ptr, int end) {
751 if (isGit(raw, ptr)) {
752 int dots = 0;
753 boolean space = false;
754 int p = end - 1;
755 for (; (ptr + 2) < p; p--) {
756 if (raw[p] == '.')
757 dots++;
758 else if (raw[p] == ' ')
759 space = true;
760 else
761 break;
762 }
763 return p == ptr + 2 && (dots == 1 || space);
764 }
765 return false;
766 }
767
768 private static char toLower(byte b) {
769 if ('A' <= b && b <= 'Z')
770 return (char) (b + ('a' - 'A'));
771 return (char) b;
772 }
773
774 private static boolean isPositiveDigit(byte b) {
775 return '1' <= b && b <= '9';
776 }
777
778
779
780
781
782
783
784
785
786 public void checkBlob(final byte[] raw) throws CorruptObjectException {
787
788 }
789
790 private String normalize(byte[] raw, int ptr, int end) {
791 String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US);
792 return macosx ? Normalizer.normalize(n) : n;
793 }
794
795 private static class Normalizer {
796
797 private static final Method normalize;
798 private static final Object nfc;
799 static {
800 Method method;
801 Object formNfc;
802 try {
803 Class<?> formClazz = Class.forName("java.text.Normalizer$Form");
804 formNfc = formClazz.getField("NFC").get(null);
805 method = Class.forName("java.text.Normalizer")
806 .getMethod("normalize", CharSequence.class, formClazz);
807 } catch (ClassNotFoundException e) {
808 method = null;
809 formNfc = null;
810 } catch (NoSuchFieldException e) {
811 method = null;
812 formNfc = null;
813 } catch (NoSuchMethodException e) {
814 method = null;
815 formNfc = null;
816 } catch (SecurityException e) {
817 method = null;
818 formNfc = null;
819 } catch (IllegalArgumentException e) {
820 method = null;
821 formNfc = null;
822 } catch (IllegalAccessException e) {
823 method = null;
824 formNfc = null;
825 }
826 normalize = method;
827 nfc = formNfc;
828 }
829
830 static String normalize(String in) {
831 if (normalize == null)
832 return in;
833 try {
834 return (String) normalize.invoke(null, in, nfc);
835 } catch (IllegalAccessException e) {
836 return in;
837 } catch (InvocationTargetException e) {
838 if (e.getCause() instanceof RuntimeException)
839 throw (RuntimeException) e.getCause();
840 if (e.getCause() instanceof Error)
841 throw (Error) e.getCause();
842 return in;
843 }
844 }
845 }
846 }