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 default:
622 return false;
623 }
624 case (byte) 0xef:
625 checkTruncatedIgnorableUTF8(raw, ptr, end);
626
627 if ((raw[ptr + 1] == (byte) 0xbb)
628 && (raw[ptr + 2] == (byte) 0xbf)) {
629 ignorable = true;
630 ptr += 3;
631 continue;
632 }
633 return false;
634 default:
635 if (g == 4)
636 return false;
637 if (raw[ptr++] != git[g++])
638 return false;
639 }
640 }
641 if (g == 4 && ignorable)
642 return true;
643 return false;
644 }
645
646 private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end)
647 throws CorruptObjectException {
648 if ((ptr + 2) >= end)
649 throw new CorruptObjectException(MessageFormat.format(
650 JGitText.get().corruptObjectInvalidNameInvalidUtf8,
651 toHexString(raw, ptr, end)));
652 }
653
654 private static String toHexString(byte[] raw, int ptr, int end) {
655 StringBuilder b = new StringBuilder("0x");
656 for (int i = ptr; i < end; i++)
657 b.append(String.format("%02x", Byte.valueOf(raw[i])));
658 return b.toString();
659 }
660
661 private static void checkNotWindowsDevice(byte[] raw, int ptr, int end)
662 throws CorruptObjectException {
663 switch (toLower(raw[ptr])) {
664 case 'a':
665 if (end - ptr >= 3
666 && toLower(raw[ptr + 1]) == 'u'
667 && toLower(raw[ptr + 2]) == 'x'
668 && (end - ptr == 3 || raw[ptr + 3] == '.'))
669 throw new CorruptObjectException(
670 JGitText.get().corruptObjectInvalidNameAux);
671 break;
672
673 case 'c':
674 if (end - ptr >= 3
675 && toLower(raw[ptr + 2]) == 'n'
676 && toLower(raw[ptr + 1]) == 'o'
677 && (end - ptr == 3 || raw[ptr + 3] == '.'))
678 throw new CorruptObjectException(
679 JGitText.get().corruptObjectInvalidNameCon);
680 if (end - ptr >= 4
681 && toLower(raw[ptr + 2]) == 'm'
682 && toLower(raw[ptr + 1]) == 'o'
683 && isPositiveDigit(raw[ptr + 3])
684 && (end - ptr == 4 || raw[ptr + 4] == '.'))
685 throw new CorruptObjectException(String.format(
686 JGitText.get().corruptObjectInvalidNameCom,
687 Character.valueOf(((char) raw[ptr + 3]))));
688 break;
689
690 case 'l':
691 if (end - ptr >= 4
692 && toLower(raw[ptr + 1]) == 'p'
693 && toLower(raw[ptr + 2]) == 't'
694 && isPositiveDigit(raw[ptr + 3])
695 && (end - ptr == 4 || raw[ptr + 4] == '.'))
696 throw new CorruptObjectException(String.format(
697 JGitText.get().corruptObjectInvalidNameLpt,
698 Character.valueOf(((char) raw[ptr + 3]))));
699 break;
700
701 case 'n':
702 if (end - ptr >= 3
703 && toLower(raw[ptr + 1]) == 'u'
704 && toLower(raw[ptr + 2]) == 'l'
705 && (end - ptr == 3 || raw[ptr + 3] == '.'))
706 throw new CorruptObjectException(
707 JGitText.get().corruptObjectInvalidNameNul);
708 break;
709
710 case 'p':
711 if (end - ptr >= 3
712 && toLower(raw[ptr + 1]) == 'r'
713 && toLower(raw[ptr + 2]) == 'n'
714 && (end - ptr == 3 || raw[ptr + 3] == '.'))
715 throw new CorruptObjectException(
716 JGitText.get().corruptObjectInvalidNamePrn);
717 break;
718 }
719 }
720
721 private static boolean isInvalidOnWindows(byte c) {
722
723 switch (c) {
724 case '"':
725 case '*':
726 case ':':
727 case '<':
728 case '>':
729 case '?':
730 case '\\':
731 case '|':
732 return true;
733 }
734 return 1 <= c && c <= 31;
735 }
736
737 private static boolean isGit(byte[] buf, int p) {
738 return toLower(buf[p]) == 'g'
739 && toLower(buf[p + 1]) == 'i'
740 && toLower(buf[p + 2]) == 't';
741 }
742
743 private static boolean isGitTilde1(byte[] buf, int p, int end) {
744 if (end - p != 5)
745 return false;
746 return toLower(buf[p]) == 'g' && toLower(buf[p + 1]) == 'i'
747 && toLower(buf[p + 2]) == 't' && buf[p + 3] == '~'
748 && buf[p + 4] == '1';
749 }
750
751 private static boolean isNormalizedGit(byte[] raw, int ptr, int end) {
752 if (isGit(raw, ptr)) {
753 int dots = 0;
754 boolean space = false;
755 int p = end - 1;
756 for (; (ptr + 2) < p; p--) {
757 if (raw[p] == '.')
758 dots++;
759 else if (raw[p] == ' ')
760 space = true;
761 else
762 break;
763 }
764 return p == ptr + 2 && (dots == 1 || space);
765 }
766 return false;
767 }
768
769 private static char toLower(byte b) {
770 if ('A' <= b && b <= 'Z')
771 return (char) (b + ('a' - 'A'));
772 return (char) b;
773 }
774
775 private static boolean isPositiveDigit(byte b) {
776 return '1' <= b && b <= '9';
777 }
778
779
780
781
782
783
784
785
786
787 public void checkBlob(final byte[] raw) throws CorruptObjectException {
788
789 }
790
791 private String normalize(byte[] raw, int ptr, int end) {
792 String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US);
793 return macosx ? Normalizer.normalize(n) : n;
794 }
795
796 private static class Normalizer {
797
798 private static final Method normalize;
799 private static final Object nfc;
800 static {
801 Method method;
802 Object formNfc;
803 try {
804 Class<?> formClazz = Class.forName("java.text.Normalizer$Form");
805 formNfc = formClazz.getField("NFC").get(null);
806 method = Class.forName("java.text.Normalizer")
807 .getMethod("normalize", CharSequence.class, formClazz);
808 } catch (ClassNotFoundException e) {
809 method = null;
810 formNfc = null;
811 } catch (NoSuchFieldException e) {
812 method = null;
813 formNfc = null;
814 } catch (NoSuchMethodException e) {
815 method = null;
816 formNfc = null;
817 } catch (SecurityException e) {
818 method = null;
819 formNfc = null;
820 } catch (IllegalArgumentException e) {
821 method = null;
822 formNfc = null;
823 } catch (IllegalAccessException e) {
824 method = null;
825 formNfc = null;
826 }
827 normalize = method;
828 nfc = formNfc;
829 }
830
831 static String normalize(String in) {
832 if (normalize == null)
833 return in;
834 try {
835 return (String) normalize.invoke(null, in, nfc);
836 } catch (IllegalAccessException e) {
837 return in;
838 } catch (InvocationTargetException e) {
839 if (e.getCause() instanceof RuntimeException)
840 throw (RuntimeException) e.getCause();
841 if (e.getCause() instanceof Error)
842 throw (Error) e.getCause();
843 return in;
844 }
845 }
846 }
847 }