检查根目录下的文件是否以可移植的方式命名

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
   }
 }