检查根目录下的文件是否以可移植的方式命名
Check if files under a root are named in a portable way
我想检查给定文件夹中的所有文件
具有可移植的名称,或者如果它们具有一些不幸的名称,可能无法在各种文件系统上表示相同的文件结构;我想至少支持最常见的情况。
例如,在 Windows 上,您不能拥有名为
aux.txt
,文件名不区分大小写。
这是我最好的尝试,但我不是操作系统和文件系统设计方面的专家。
在维基百科上,我找到了 'incomplete' 个可能问题的列表……但是……我怎样才能发现所有问题?
请查看我下面的代码,看看我是否忘记了任何微妙的不幸案例。特别是,我发现了很多 'Windows issues'。有什么 Linux/Mac 我应该检查的问题吗?
class CheckFileSystemPortable {
Path top;
List<Path> okPaths=new ArrayList<>();
List<Path> badPaths=new ArrayList<>();
List<Path> repeatedPaths=new ArrayList<>();
CheckFileSystemPortable(Path top){
assert Files.isDirectory(top);
this.top=top;
try (Stream<Path> walk = Files.walk(top)) {//the first one is guaranteed to be the root
walk.skip(1).forEach(this::checkSystemIndependentPath);
} catch (IOException e) {
throw new Error(e);
}
for(var p:okPaths) {
checkRepeatedPaths(p);
}
okPaths.removeAll(repeatedPaths);
}
private void checkRepeatedPaths(Path p) {
var s=p.toString();
for(var pi:okPaths){
if (pi!=p && pi.toString().equalsIgnoreCase(s)) {
repeatedPaths.add(pi);
}
}
}
//incomplete list from wikipedia below:
//https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
private static final List<String>forbiddenWin=List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse"
);
private void checkSystemIndependentPath(Path path) {
String lastName=path.getName(path.getNameCount()-1).toString();
String[] parts=lastName.split("\.");
var ko = forbiddenWin.stream()
.filter(f -> Stream.of(parts).anyMatch(p->p.equalsIgnoreCase(f)))
.count();
if(ko!=0) {
badPaths.add(path);
} else {
okPaths.add(path);
}
}
}
如果我通过阅读 Filename 维基百科页面正确理解了您的问题,可移植文件名必须:
- 是posix compliant。例如。字母数字 ascii 字符和
_
、-
- 避免使用 windows 和 DOS 设备名称。
- 避免 NTFS 特殊名称。
- 避免使用特殊字符。例如。
\
、|
、/
、$
等
- 避免尾随 space 或点。
- 避免文件名以
-
. 开头
- 必须达到最大长度。例如。 8 位 Fat 的最大长度为 9 个字符。
- 有些系统需要一个带有
.
的扩展名,然后是一个 3 个字母的扩展名。
考虑到所有这些,checkSystemIndependentPath
可以稍微简化一下,以使用正则表达式涵盖大多数情况。
例如POSIX文件名,不包括特殊设备、NTFS、特殊字符和尾随space或点:
private void checkSystemIndependentPath(Path path){
String reserved = "^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)*$";
String posix = "^[a-zA-Z\._-]+$";
String trailing = ".*[\s|\.]$";
int nameLimit = 9;
String fileName = path.getFileName().toString();
if (fileName.matches(posix) &&
!fileName.matches(reserved) &&
!fileName.matches(trailing) &&
fileName.length() <= nameLimit) {
okPaths.add(path);
} else {
badPaths.add(path);
}
}
请注意,该示例未经测试且未涵盖边缘条件。
例如,某些系统禁止在目录名称中使用点。
某些系统会抱怨文件名中有多个点。
假设您的 windows 禁止列表是正确的,并添加“:”(mac)和 nul
(所有地方),使用正则表达式!
private static final List<String> FORBIDDEN_WINDOWS_NAMES = List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse"
); // you can add more
private static final String FORBIDDEN_CHARACTERS = "[=10=]:"; // you can add more
private static final String REGEX = "^(?i)(?!.*[" + FORBIDDEN_CHARACTERS + "])(.*/)?(?!(\Q" +
String.join("\E|\Q", FORBIDDEN_WINDOWS_NAMES) + "\E)(\.[^/]*)?$).*";
private static Pattern ALLOWED_PATTERN = Pattern.compile(REGEX);
public static boolean isAllowed(String path) {
return ALLOWED_PATTERN.matcher(path).matches();
}
fyi,从此处定义的 lists/chars 生成的正则表达式是:
^(?i)(?!.*[<nul>:])(.*/)?(?!(\QCON\E|\QPRN\E|\QAUX\E|\QCLOCK$\E|\QNUL\E|\QCOM0\E|\QCOM1\E|\QCOM2\E|\QCOM3\E|\QCOM4\E|\QCOM5\E|\QCOM6\E|\QCOM7\E|\QCOM8\E|\QCOM9\E|\QLPT0\E|\QLPT1\E|\QLPT2\E|\QLPT3\E|\QLPT4\E|\QLPT5\E|\QLPT6\E|\QLPT7\E|\QLPT8\E|\QLPT9\E|\QLST\E|\QKEYBD$\E|\QSCREEN$\E|\Q$IDLE$\E|\QCONFIG$\E|\Q$Mft\E|\Q$MftMirr\E|\Q$LogFile\E|\Q$Volume\E|\Q$AttrDef\E|\Q$Bitmap\E|\Q$Boot\E|\Q$BadClus\E|\Q$Secure\E|\Q$Upcase\E|\Q$Extend\E|\Q$Quota\E|\Q$ObjId\E|\Q$Reparse\E)(\.[^/]*)?$).*
每个禁止的文件名都包含在 \Q
和 \E
中,这就是您在正则表达式中 引用 表达式的方式,因此所有字符都被视为文字字符。例如,\Q$Boot\E
中的美元符号并不表示输入结束,它只是一个普通的美元符号。
谢谢大家。
我现在已经为此制作了完整的代码,
我将它作为一个潜在的答案分享,因为我认为我必须走的天平很可能很常见。
要点:
- 我不得不选择 248 作为最大尺寸
- 我必须接受文件名中的“$”。
- 我必须完全跳过任何标记为隐藏(获胜)或以“.”开头的 file/folder/subtree;这些文件是隐藏的,很可能是自动生成的,不在我的范围内
控制,反正我的应用程序没有使用。
- 当然,如果您的应用程序依赖于“.**”files/folders,您可能需要检查这些。
- 另一个摩擦点是多点:不仅有的系统可能会被打乱,而且还不清楚分机从哪里开始,主名从哪里结束。
例如,我有一个用例,其中包含文件 derby-10.15.2.0.jar 。
扩展名是 .jar 还是 .15.2.0.jar?某些系统不同意这一点吗?
现在,我强制将这些文件重命名为 derby-10_15_2_0.jar
public class CheckFileSystemPortable{
Path top;
List<Path> okPaths = new ArrayList<>();
List<Path> badPaths = new ArrayList<>();
List<Path> repeatedPaths = new ArrayList<>();
public void makeError(..) {..anything you need for a good message..}
public boolean isDirectory(Path top){ return Files.isDirectory(top); }
//I override the above when I do mocks for testing
public CheckFileSystemPortable(Path top){
assert isDirectory(top);
this.top = top;
walkIn1(top);
for(var p:okPaths){ checkRepeatedPaths(p); }
okPaths.removeAll(repeatedPaths);
}
public void walkIn1(Path path) {
try(Stream<Path> walk = Files.walk(path,1)){
//the first one is guaranteed to be the root
walk.skip(1).forEach(this::checkSystemIndependentPath);
}
catch(IOException e){ throw /*unreachable*/; }
}
private void checkRepeatedPaths(Path p){
var s = p.toString();
for(var pi:okPaths){
if (pi!=p && pi.toString().equalsIgnoreCase(s)) {repeatedPaths.add(pi);}
}
}
private static final List<String>forbiddenWin = List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse",
""
);
static final Pattern regex = Pattern.compile(//POSIX + $,
"^[a-zA-Z0-9\_\-\$]+$");// but . is handled separately
public void checkSystemIndependentPath(Path path){
String lastName=path.getFileName().toString();
//too dangerous even for ignored ones
if(lastName.equals(".") || lastName.equals("..")) { badPaths.add(path); return; }
boolean skip = path.toFile().isHidden() || lastName.startsWith(".");
if(skip){ return; }
var badSizeEndStart = lastName.length()>248
||lastName.endsWith(".")
||lastName.endsWith("-")
|| lastName.startsWith("-");
if(badSizeEndStart){ badPaths.add(path); return; }
var i=lastName.indexOf(".");
var fileName = i==-1?lastName:lastName.substring(0,i);
var extension = i==-1?"":lastName.substring(i+1);
var extensionDots = extension.contains(".");
if(extensionDots){ badPaths.add(path); return; }
var badDir = isDirectory(path) && i!=-1;
if(badDir){ badPaths.add(path); return; }
var badFileName = !regex.matcher(fileName).matches();
var badExtension = !extension.isEmpty() && !regex.matcher(extension).matches();
if(badFileName||badExtension){ badPaths.add(path); return; }
var ko = forbiddenWin.stream()
.filter(f->fileName.equalsIgnoreCase(f)).count();
if(ko!=0){ badPaths.add(path); return; }
okPaths.add(path);
walkIn1(path);//recursive exploration
}
}
我想检查给定文件夹中的所有文件
具有可移植的名称,或者如果它们具有一些不幸的名称,可能无法在各种文件系统上表示相同的文件结构;我想至少支持最常见的情况。
例如,在 Windows 上,您不能拥有名为
aux.txt
,文件名不区分大小写。
这是我最好的尝试,但我不是操作系统和文件系统设计方面的专家。
在维基百科上,我找到了 'incomplete' 个可能问题的列表……但是……我怎样才能发现所有问题?
请查看我下面的代码,看看我是否忘记了任何微妙的不幸案例。特别是,我发现了很多 'Windows issues'。有什么 Linux/Mac 我应该检查的问题吗?
class CheckFileSystemPortable {
Path top;
List<Path> okPaths=new ArrayList<>();
List<Path> badPaths=new ArrayList<>();
List<Path> repeatedPaths=new ArrayList<>();
CheckFileSystemPortable(Path top){
assert Files.isDirectory(top);
this.top=top;
try (Stream<Path> walk = Files.walk(top)) {//the first one is guaranteed to be the root
walk.skip(1).forEach(this::checkSystemIndependentPath);
} catch (IOException e) {
throw new Error(e);
}
for(var p:okPaths) {
checkRepeatedPaths(p);
}
okPaths.removeAll(repeatedPaths);
}
private void checkRepeatedPaths(Path p) {
var s=p.toString();
for(var pi:okPaths){
if (pi!=p && pi.toString().equalsIgnoreCase(s)) {
repeatedPaths.add(pi);
}
}
}
//incomplete list from wikipedia below:
//https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
private static final List<String>forbiddenWin=List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse"
);
private void checkSystemIndependentPath(Path path) {
String lastName=path.getName(path.getNameCount()-1).toString();
String[] parts=lastName.split("\.");
var ko = forbiddenWin.stream()
.filter(f -> Stream.of(parts).anyMatch(p->p.equalsIgnoreCase(f)))
.count();
if(ko!=0) {
badPaths.add(path);
} else {
okPaths.add(path);
}
}
}
如果我通过阅读 Filename 维基百科页面正确理解了您的问题,可移植文件名必须:
- 是posix compliant。例如。字母数字 ascii 字符和
_
、-
- 避免使用 windows 和 DOS 设备名称。
- 避免 NTFS 特殊名称。
- 避免使用特殊字符。例如。
\
、|
、/
、$
等 - 避免尾随 space 或点。
- 避免文件名以
-
. 开头
- 必须达到最大长度。例如。 8 位 Fat 的最大长度为 9 个字符。
- 有些系统需要一个带有
.
的扩展名,然后是一个 3 个字母的扩展名。
考虑到所有这些,checkSystemIndependentPath
可以稍微简化一下,以使用正则表达式涵盖大多数情况。
例如POSIX文件名,不包括特殊设备、NTFS、特殊字符和尾随space或点:
private void checkSystemIndependentPath(Path path){
String reserved = "^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)*$";
String posix = "^[a-zA-Z\._-]+$";
String trailing = ".*[\s|\.]$";
int nameLimit = 9;
String fileName = path.getFileName().toString();
if (fileName.matches(posix) &&
!fileName.matches(reserved) &&
!fileName.matches(trailing) &&
fileName.length() <= nameLimit) {
okPaths.add(path);
} else {
badPaths.add(path);
}
}
请注意,该示例未经测试且未涵盖边缘条件。 例如,某些系统禁止在目录名称中使用点。 某些系统会抱怨文件名中有多个点。
假设您的 windows 禁止列表是正确的,并添加“:”(mac)和 nul
(所有地方),使用正则表达式!
private static final List<String> FORBIDDEN_WINDOWS_NAMES = List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse"
); // you can add more
private static final String FORBIDDEN_CHARACTERS = "[=10=]:"; // you can add more
private static final String REGEX = "^(?i)(?!.*[" + FORBIDDEN_CHARACTERS + "])(.*/)?(?!(\Q" +
String.join("\E|\Q", FORBIDDEN_WINDOWS_NAMES) + "\E)(\.[^/]*)?$).*";
private static Pattern ALLOWED_PATTERN = Pattern.compile(REGEX);
public static boolean isAllowed(String path) {
return ALLOWED_PATTERN.matcher(path).matches();
}
fyi,从此处定义的 lists/chars 生成的正则表达式是:
^(?i)(?!.*[<nul>:])(.*/)?(?!(\QCON\E|\QPRN\E|\QAUX\E|\QCLOCK$\E|\QNUL\E|\QCOM0\E|\QCOM1\E|\QCOM2\E|\QCOM3\E|\QCOM4\E|\QCOM5\E|\QCOM6\E|\QCOM7\E|\QCOM8\E|\QCOM9\E|\QLPT0\E|\QLPT1\E|\QLPT2\E|\QLPT3\E|\QLPT4\E|\QLPT5\E|\QLPT6\E|\QLPT7\E|\QLPT8\E|\QLPT9\E|\QLST\E|\QKEYBD$\E|\QSCREEN$\E|\Q$IDLE$\E|\QCONFIG$\E|\Q$Mft\E|\Q$MftMirr\E|\Q$LogFile\E|\Q$Volume\E|\Q$AttrDef\E|\Q$Bitmap\E|\Q$Boot\E|\Q$BadClus\E|\Q$Secure\E|\Q$Upcase\E|\Q$Extend\E|\Q$Quota\E|\Q$ObjId\E|\Q$Reparse\E)(\.[^/]*)?$).*
每个禁止的文件名都包含在 \Q
和 \E
中,这就是您在正则表达式中 引用 表达式的方式,因此所有字符都被视为文字字符。例如,\Q$Boot\E
中的美元符号并不表示输入结束,它只是一个普通的美元符号。
谢谢大家。 我现在已经为此制作了完整的代码, 我将它作为一个潜在的答案分享,因为我认为我必须走的天平很可能很常见。 要点:
- 我不得不选择 248 作为最大尺寸
- 我必须接受文件名中的“$”。
- 我必须完全跳过任何标记为隐藏(获胜)或以“.”开头的 file/folder/subtree;这些文件是隐藏的,很可能是自动生成的,不在我的范围内 控制,反正我的应用程序没有使用。
- 当然,如果您的应用程序依赖于“.**”files/folders,您可能需要检查这些。
- 另一个摩擦点是多点:不仅有的系统可能会被打乱,而且还不清楚分机从哪里开始,主名从哪里结束。 例如,我有一个用例,其中包含文件 derby-10.15.2.0.jar 。 扩展名是 .jar 还是 .15.2.0.jar?某些系统不同意这一点吗? 现在,我强制将这些文件重命名为 derby-10_15_2_0.jar
public class CheckFileSystemPortable{
Path top;
List<Path> okPaths = new ArrayList<>();
List<Path> badPaths = new ArrayList<>();
List<Path> repeatedPaths = new ArrayList<>();
public void makeError(..) {..anything you need for a good message..}
public boolean isDirectory(Path top){ return Files.isDirectory(top); }
//I override the above when I do mocks for testing
public CheckFileSystemPortable(Path top){
assert isDirectory(top);
this.top = top;
walkIn1(top);
for(var p:okPaths){ checkRepeatedPaths(p); }
okPaths.removeAll(repeatedPaths);
}
public void walkIn1(Path path) {
try(Stream<Path> walk = Files.walk(path,1)){
//the first one is guaranteed to be the root
walk.skip(1).forEach(this::checkSystemIndependentPath);
}
catch(IOException e){ throw /*unreachable*/; }
}
private void checkRepeatedPaths(Path p){
var s = p.toString();
for(var pi:okPaths){
if (pi!=p && pi.toString().equalsIgnoreCase(s)) {repeatedPaths.add(pi);}
}
}
private static final List<String>forbiddenWin = List.of(
"CON", "PRN", "AUX", "CLOCK$", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
"LST", "KEYBD$", "SCREEN$", "$IDLE$", "CONFIG$",
"$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot",
"$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse",
""
);
static final Pattern regex = Pattern.compile(//POSIX + $,
"^[a-zA-Z0-9\_\-\$]+$");// but . is handled separately
public void checkSystemIndependentPath(Path path){
String lastName=path.getFileName().toString();
//too dangerous even for ignored ones
if(lastName.equals(".") || lastName.equals("..")) { badPaths.add(path); return; }
boolean skip = path.toFile().isHidden() || lastName.startsWith(".");
if(skip){ return; }
var badSizeEndStart = lastName.length()>248
||lastName.endsWith(".")
||lastName.endsWith("-")
|| lastName.startsWith("-");
if(badSizeEndStart){ badPaths.add(path); return; }
var i=lastName.indexOf(".");
var fileName = i==-1?lastName:lastName.substring(0,i);
var extension = i==-1?"":lastName.substring(i+1);
var extensionDots = extension.contains(".");
if(extensionDots){ badPaths.add(path); return; }
var badDir = isDirectory(path) && i!=-1;
if(badDir){ badPaths.add(path); return; }
var badFileName = !regex.matcher(fileName).matches();
var badExtension = !extension.isEmpty() && !regex.matcher(extension).matches();
if(badFileName||badExtension){ badPaths.add(path); return; }
var ko = forbiddenWin.stream()
.filter(f->fileName.equalsIgnoreCase(f)).count();
if(ko!=0){ badPaths.add(path); return; }
okPaths.add(path);
walkIn1(path);//recursive exploration
}
}