Mac's HFS+ folds concatentations of ".git" and ignorable Unicode characters [1] to ".git" [2]. Hence we need to disallow all names which could potentially be a shortname for ".git". Example: in an empty directory create a folder ".g\U+200Cit". Now you can't create another folder ".git". The following characters are ignorable Unicode which are ignored on HFS+: unicode hex name ------------------------------------------------- U+200C 0xe2808c ZERO WIDTH NON-JOINER U+200D 0xe2808d ZERO WIDTH JOINER U+200E 0xe2808e LEFT-TO-RIGHT MARK U+200F 0xe2808f RIGHT-TO-LEFT MARK U+202A 0xe280aa LEFT-TO-RIGHT EMBEDDING U+202B 0xe280ab RIGHT-TO-LEFT EMBEDDING U+202C 0xe280ac POP DIRECTIONAL FORMATTING U+202D 0xe280ad LEFT-TO-RIGHT OVERRIDE U+202E 0xe280ae RIGHT-TO-LEFT OVERRIDE U+206A 0xe281aa INHIBIT SYMMETRIC SWAPPING U+206B 0xe281ab ACTIVATE SYMMETRIC SWAPPING U+206C 0xe281ac INHIBIT ARABIC FORM SHAPING U+206D 0xe281ad ACTIVATE ARABIC FORM SHAPING U+206E 0xe281ae NATIONAL DIGIT SHAPES U+206F 0xe281af NOMINAL DIGIT SHAPES U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE [1] http://www.unicode.org/versions/Unicode7.0.0/ch05.pdf#G40025 http://www.unicode.org/reports/tr31/#Layout_and_Format_Control_Characters [2] http://dubeiko.com/development/FileSystems/HFSPLUS/tn1150.html#UnicodeSubtleties Change-Id: Ib6a1dd090b2649bdd8ec16387c994ed29de2860d Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>tags/v3.4.2.201412180340-r
@@ -1307,6 +1307,126 @@ public class ObjectCheckerTest { | |||
} | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsMacHFSGit() { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 .gi\u200Ct"); | |||
byte[] data = Constants.encode(b.toString()); | |||
try { | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
fail("incorrectly accepted an invalid tree"); | |||
} catch (CorruptObjectException e) { | |||
assertEquals( | |||
"invalid name '.gi\u200Ct' contains ignorable Unicode characters", | |||
e.getMessage()); | |||
} | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsMacHFSGit2() { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 \u206B.git"); | |||
byte[] data = Constants.encode(b.toString()); | |||
try { | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
fail("incorrectly accepted an invalid tree"); | |||
} catch (CorruptObjectException e) { | |||
assertEquals( | |||
"invalid name '\u206B.git' contains ignorable Unicode characters", | |||
e.getMessage()); | |||
} | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsMacHFSGit3() { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 .git\uFEFF"); | |||
byte[] data = Constants.encode(b.toString()); | |||
try { | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
fail("incorrectly accepted an invalid tree"); | |||
} catch (CorruptObjectException e) { | |||
assertEquals( | |||
"invalid name '.git\uFEFF' contains ignorable Unicode characters", | |||
e.getMessage()); | |||
} | |||
} | |||
private static byte[] concat(byte[] b1, byte[] b2) { | |||
byte[] data = new byte[b1.length + b2.length]; | |||
System.arraycopy(b1, 0, data, 0, b1.length); | |||
System.arraycopy(b2, 0, data, b1.length, b2.length); | |||
return data; | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd() { | |||
byte[] data = concat(Constants.encode("100644 .git"), | |||
new byte[] { (byte) 0xef }); | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, ""); | |||
data = concat(data, Constants.encode(b.toString())); | |||
try { | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
fail("incorrectly accepted an invalid tree"); | |||
} catch (CorruptObjectException e) { | |||
assertEquals( | |||
"invalid name contains byte sequence '0xef' which is not a valid UTF-8 character", | |||
e.getMessage()); | |||
} | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2() { | |||
byte[] data = concat(Constants.encode("100644 .git"), new byte[] { | |||
(byte) 0xe2, (byte) 0xab }); | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, ""); | |||
data = concat(data, Constants.encode(b.toString())); | |||
try { | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
fail("incorrectly accepted an invalid tree"); | |||
} catch (CorruptObjectException e) { | |||
assertEquals( | |||
"invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character", | |||
e.getMessage()); | |||
} | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsNotMacHFSGit() | |||
throws CorruptObjectException { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 .git\u200Cx"); | |||
byte[] data = Constants.encode(b.toString()); | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsNotMacHFSGit2() | |||
throws CorruptObjectException { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 .kit\u200C"); | |||
byte[] data = Constants.encode(b.toString()); | |||
checker.setSafeForMacOS(true); | |||
checker.checkTree(data); | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsNotMacHFSGitOtherPlatform() | |||
throws CorruptObjectException { | |||
StringBuilder b = new StringBuilder(); | |||
entry(b, "100644 .git\u200C"); | |||
byte[] data = Constants.encode(b.toString()); | |||
checker.checkTree(data); | |||
} | |||
@Test | |||
public void testInvalidTreeNameIsDotGitDot() { | |||
StringBuilder b = new StringBuilder(); |
@@ -469,6 +469,11 @@ public class ObjectChecker { | |||
RawParseUtils.decode(raw, ptr, end))); | |||
} | |||
if (macosx && isMacHFSGit(raw, ptr, end)) | |||
throw new CorruptObjectException(String.format( | |||
"invalid name '%s' contains ignorable Unicode characters", | |||
RawParseUtils.decode(raw, ptr, end))); | |||
if (windows) { | |||
// Windows ignores space and dot at end of file name. | |||
if (raw[end - 1] == ' ' || raw[end - 1] == '.') | |||
@@ -479,6 +484,88 @@ public class ObjectChecker { | |||
} | |||
} | |||
// Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters | |||
// to ".git" therefore we should prevent such names | |||
private static boolean isMacHFSGit(byte[] raw, int ptr, int end) | |||
throws CorruptObjectException { | |||
boolean ignorable = false; | |||
byte[] git = new byte[] { '.', 'g', 'i', 't' }; | |||
int g = 0; | |||
while (ptr < end) { | |||
switch (raw[ptr]) { | |||
case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192 | |||
checkTruncatedIgnorableUTF8(raw, ptr, end); | |||
switch (raw[ptr + 1]) { | |||
case (byte) 0x80: | |||
switch (raw[ptr + 2]) { | |||
case (byte) 0x8c: // U+200C 0xe2808c ZERO WIDTH NON-JOINER | |||
case (byte) 0x8d: // U+200D 0xe2808d ZERO WIDTH JOINER | |||
case (byte) 0x8e: // U+200E 0xe2808e LEFT-TO-RIGHT MARK | |||
case (byte) 0x8f: // U+200F 0xe2808f RIGHT-TO-LEFT MARK | |||
case (byte) 0xaa: // U+202A 0xe280aa LEFT-TO-RIGHT EMBEDDING | |||
case (byte) 0xab: // U+202B 0xe280ab RIGHT-TO-LEFT EMBEDDING | |||
case (byte) 0xac: // U+202C 0xe280ac POP DIRECTIONAL FORMATTING | |||
case (byte) 0xad: // U+202D 0xe280ad LEFT-TO-RIGHT OVERRIDE | |||
case (byte) 0xae: // U+202E 0xe280ae RIGHT-TO-LEFT OVERRIDE | |||
ignorable = true; | |||
ptr += 3; | |||
continue; | |||
default: | |||
return false; | |||
} | |||
case (byte) 0x81: | |||
switch (raw[ptr + 2]) { | |||
case (byte) 0xaa: // U+206A 0xe281aa INHIBIT SYMMETRIC SWAPPING | |||
case (byte) 0xab: // U+206B 0xe281ab ACTIVATE SYMMETRIC SWAPPING | |||
case (byte) 0xac: // U+206C 0xe281ac INHIBIT ARABIC FORM SHAPING | |||
case (byte) 0xad: // U+206D 0xe281ad ACTIVATE ARABIC FORM SHAPING | |||
case (byte) 0xae: // U+206E 0xe281ae NATIONAL DIGIT SHAPES | |||
case (byte) 0xaf: // U+206F 0xe281af NOMINAL DIGIT SHAPES | |||
ignorable = true; | |||
ptr += 3; | |||
continue; | |||
default: | |||
return false; | |||
} | |||
} | |||
break; | |||
case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024 | |||
checkTruncatedIgnorableUTF8(raw, ptr, end); | |||
// U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE | |||
if ((raw[ptr + 1] == (byte) 0xbb) | |||
&& (raw[ptr + 2] == (byte) 0xbf)) { | |||
ignorable = true; | |||
ptr += 3; | |||
continue; | |||
} | |||
return false; | |||
default: | |||
if (g == 4) | |||
return false; | |||
if (raw[ptr++] != git[g++]) | |||
return false; | |||
} | |||
} | |||
if (g == 4 && ignorable) | |||
return true; | |||
return false; | |||
} | |||
private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end) | |||
throws CorruptObjectException { | |||
if ((ptr + 2) >= end) | |||
throw new CorruptObjectException(MessageFormat.format( | |||
"invalid name contains byte sequence ''{0}'' which is not a valid UTF-8 character", | |||
toHexString(raw, ptr, end))); | |||
} | |||
private static String toHexString(byte[] raw, int ptr, int end) { | |||
StringBuilder b = new StringBuilder("0x"); //$NON-NLS-1$ | |||
for (int i = ptr; i < end; i++) | |||
b.append(String.format("%02x", Byte.valueOf(raw[i]))); //$NON-NLS-1$ | |||
return b.toString(); | |||
} | |||
private static void checkNotWindowsDevice(byte[] raw, int ptr, int end) | |||
throws CorruptObjectException { | |||
switch (toLower(raw[ptr])) { |