Làm cách nào để mã hóa một chuỗi trong Java một cách an toàn để sử dụng làm tên tệp?


117

Tôi nhận được một chuỗi từ một quy trình bên ngoài. Tôi muốn sử dụng Chuỗi đó để tạo tên tệp, rồi ghi vào tệp đó. Đây là đoạn mã của tôi để thực hiện việc này:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), s);
    PrintWriter currentWriter = new PrintWriter(currentFile);

Nếu s chứa một ký tự không hợp lệ, chẳng hạn như '/' trong Hệ điều hành dựa trên Unix, thì java.io.FileNotFoundException sẽ được ném ra (đúng).

Làm cách nào để tôi có thể mã hóa chuỗi một cách an toàn để nó có thể được sử dụng làm tên tệp?

Chỉnh sửa: Điều tôi hy vọng là một lệnh gọi API thực hiện điều này cho tôi.

Tôi có thể làm điều này:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), URLEncoder.encode(s, "UTF-8"));
    PrintWriter currentWriter = new PrintWriter(currentFile);

Nhưng tôi không chắc liệu URLEncoder có đáng tin cậy cho mục đích này hay không.


1
Mục đích của việc mã hóa chuỗi là gì?
Stephen C,

3
@Stephen C: Mục đích của việc mã hóa chuỗi là để phù hợp để sử dụng làm tên tệp, như java.net.URLEncoder làm cho URL.
Steve McLeod

1
Ồ, tôi hiểu rồi. Có cần phải đảo ngược mã hóa không?
Stephen C,

@Stephen C: Không, nó không cần phải đảo ngược, nhưng tôi muốn kết quả giống với chuỗi gốc nhất có thể.
Steve McLeod

1
Bảng mã có cần làm mờ tên gốc không? Nó có cần phải là 1-1; tức là va chạm OK?
Stephen C

Câu trả lời:


17

Nếu bạn muốn kết quả giống với tệp gốc, SHA-1 hoặc bất kỳ lược đồ băm nào khác không phải là câu trả lời. Nếu phải tránh va chạm, thì việc thay thế hoặc loại bỏ các ký tự "xấu" đơn giản cũng không phải là câu trả lời.

Thay vào đó bạn muốn một cái gì đó như thế này. (Lưu ý: đây nên được coi là một ví dụ minh họa, không phải là thứ để sao chép và dán.)

char fileSep = '/'; // ... or do this portably.
char escape = '%'; // ... or some other legal char.
String s = ...
int len = s.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
    char ch = s.charAt(i);
    if (ch < ' ' || ch >= 0x7F || ch == fileSep || ... // add other illegal chars
        || (ch == '.' && i == 0) // we don't want to collide with "." or ".."!
        || ch == escape) {
        sb.append(escape);
        if (ch < 0x10) {
            sb.append('0');
        }
        sb.append(Integer.toHexString(ch));
    } else {
        sb.append(ch);
    }
}
File currentFile = new File(System.getProperty("user.home"), sb.toString());
PrintWriter currentWriter = new PrintWriter(currentFile);

Giải pháp này cung cấp một mã hóa có thể đảo ngược (không có xung đột) trong đó các chuỗi được mã hóa giống với các chuỗi gốc trong hầu hết các trường hợp. Tôi giả định rằng bạn đang sử dụng các ký tự 8 bit.

URLEncoder hoạt động, nhưng nó có nhược điểm là nó mã hóa rất nhiều ký tự tên tệp hợp pháp.

Nếu bạn muốn một giải pháp không được đảm bảo-có thể đảo ngược, thì chỉ cần loại bỏ các ký tự 'xấu' thay vì thay thế chúng bằng các chuỗi thoát.


Đảo ngược của mã hóa ở trên phải được thực hiện đều đặn.


105

Đề xuất của tôi là thực hiện cách tiếp cận "danh sách trắng", nghĩa là đừng thử và lọc ra những ký tự xấu. Thay vào đó, hãy xác định điều gì là OK. Bạn có thể từ chối hoặc lọc tên tệp. Nếu bạn muốn lọc nó:

String name = s.replaceAll("\\W+", "");

Điều này làm là thay thế bất kỳ ký tự nào không phải là số, chữ cái hoặc dấu gạch dưới bằng không. Ngoài ra, bạn có thể thay thế chúng bằng một ký tự khác (như dấu gạch dưới).

Vấn đề là nếu đây là một thư mục chia sẻ thì bạn không muốn xung đột tên tệp. Ngay cả khi khu vực lưu trữ của người dùng được người dùng tách biệt, bạn có thể kết thúc với một tên tệp xung đột chỉ bằng cách lọc ra các ký tự xấu. Tên mà người dùng đặt thường hữu ích nếu họ cũng muốn tải xuống.

Vì lý do này, tôi có xu hướng cho phép người dùng nhập những gì họ muốn, lưu trữ tên tệp dựa trên một lược đồ do chính tôi chọn (ví dụ: userId_fileId) và sau đó lưu trữ tên tệp của người dùng trong bảng cơ sở dữ liệu. Bằng cách đó, bạn có thể hiển thị lại nó cho người dùng, lưu trữ những thứ bạn muốn và bạn không ảnh hưởng đến bảo mật hoặc xóa sạch các tệp khác.

Bạn cũng có thể băm tệp (ví dụ băm MD5) nhưng sau đó bạn không thể liệt kê các tệp mà người dùng đưa vào (dù sao cũng không phải tên có ý nghĩa).

CHỈNH SỬA: Đã sửa lỗi regex cho java


Tôi không nghĩ rằng đó là một ý kiến ​​hay khi đưa ra giải pháp tồi trước. Ngoài ra, MD5 là một thuật toán băm gần như đã bị bẻ khóa. Tôi khuyên bạn nên sử dụng ít nhất SHA-1 hoặc tốt hơn.
vog

19
Với mục đích tạo một tên tệp duy nhất, ai quan tâm đến việc thuật toán có bị "hỏng" không?
cletus

3
@cletus: vấn đề là các chuỗi khác nhau sẽ ánh xạ đến cùng một tên tệp; tức là va chạm.
Stephen C,

3
Một vụ va chạm sẽ phải có chủ ý, câu hỏi ban đầu không nói về việc những chuỗi này được chọn bởi kẻ tấn công.
tialaramex

8
Bạn cần sử dụng "\\W+"cho regexp trong Java. Dấu gạch chéo ngược trước tiên áp dụng cho chính chuỗi và \Wkhông phải là chuỗi thoát hợp lệ. Tôi đã cố chỉnh sửa câu trả lời, nhưng có vẻ như ai đó đã từ chối chỉnh sửa của tôi :(
vadipp

35

Nó phụ thuộc vào việc mã hóa có thể đảo ngược hay không.

Có thể đảo ngược

Sử dụng mã hóa URL ( java.net.URLEncoder) để thay thế các ký tự đặc biệt bằng %xx. Lưu ý rằng bạn đề phòng các trường hợp đặc biệt trong đó chuỗi bằng ., bằng ..hoặc trống! ¹ Nhiều chương trình sử dụng mã hóa URL để tạo tên tệp, vì vậy, đây là một kỹ thuật tiêu chuẩn mà mọi người đều hiểu.

Không thể đảo ngược

Sử dụng một hàm băm (ví dụ SHA-1) của chuỗi đã cho. Các thuật toán băm hiện đại ( không phải MD5) có thể được coi là không có va chạm. Trên thực tế, bạn sẽ có một đột phá trong lĩnh vực mật mã nếu bạn phát hiện ra một vụ va chạm.


¹ Bạn có thể xử lý cả 3 trường hợp đặc biệt một cách thanh lịch bằng cách sử dụng tiền tố chẳng hạn như "myApp-". Nếu bạn đặt tệp trực tiếp vào $HOME, bạn vẫn phải làm điều đó để tránh xung đột với các tệp hiện có như ".bashrc".
public static String encodeFilename(String s)
{
    try
    {
        return "myApp-" + java.net.URLEncoder.encode(s, "UTF-8");
    }
    catch (java.io.UnsupportedEncodingException e)
    {
        throw new RuntimeException("UTF-8 is an unknown encoding!?");
    }
}


2
Ý tưởng của URLEncoder về ký tự đặc biệt là gì có thể không đúng.
Stephen C

4
@vog: URLEncoder không thành công cho "." và "..". Đây phải được mã hóa hoặc nếu không bạn sẽ va chạm với mục thư mục trong $ HOME
Stephen C

6
@vog: "*" chỉ được phép trong hầu hết các hệ thống tệp dựa trên Unix, NTFS và FAT32 không hỗ trợ nó.
Jonathan

1
"." và ".." có thể được xử lý bằng cách thoát dấu chấm thành% 2E khi chuỗi chỉ là dấu chấm (nếu bạn muốn giảm thiểu các chuỗi thoát). '*' cũng có thể được thay thế bằng "% 2A".
viphe

1
lưu ý rằng bất kỳ phương pháp mà kéo dài tên tập tin (bằng cách thay đổi nhân vật duy nhất để% 20 hoặc bất kỳ) sẽ làm mất hiệu lực một số tên tập tin đó là gần với giới hạn độ dài (255 ký tự cho các hệ thống Unix)
smcg

24

Đây là những gì tôi sử dụng:

public String sanitizeFilename(String inputName) {
    return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_");
}

Điều này làm là thay thế mọi ký tự không phải là chữ cái, số, dấu gạch dưới hoặc dấu chấm bằng dấu gạch dưới, sử dụng regex.

Điều này có nghĩa là một cái gì đó như "Làm thế nào để chuyển đổi £ thành $" sẽ trở thành "How_to_convert___to__". Phải thừa nhận rằng kết quả này không thân thiện với người dùng lắm, nhưng nó an toàn và các tên thư mục / tệp kết quả được đảm bảo hoạt động ở mọi nơi. Trong trường hợp của tôi, kết quả không được hiển thị cho người dùng và do đó không phải là vấn đề, nhưng bạn có thể muốn thay đổi regex để dễ dàng hơn.

Cần lưu ý rằng một vấn đề khác mà tôi gặp phải là đôi khi tôi sẽ nhận được các tên giống hệt nhau (vì nó dựa trên đầu vào của người dùng), vì vậy bạn nên biết điều đó, vì bạn không thể có nhiều thư mục / tệp có cùng tên trong một thư mục . Tôi chỉ nhập trước ngày và giờ hiện tại và một chuỗi ngẫu nhiên ngắn để tránh điều đó. (một chuỗi ngẫu nhiên thực tế, không phải là một hàm băm của tên tệp, vì các tên tệp giống nhau sẽ dẫn đến các hàm băm giống nhau)

Ngoài ra, bạn có thể cần phải cắt bớt hoặc rút ngắn chuỗi kết quả, vì nó có thể vượt quá giới hạn 255 ký tự mà một số hệ thống có.


6
Một vấn đề khác là nó dành riêng cho các ngôn ngữ sử dụng ký tự ASCII. Đối với các ngôn ngữ khác, nó sẽ dẫn đến tên tệp không chứa gì ngoài dấu gạch dưới.
Andy Thomas

13

Đối với những người đang tìm kiếm một giải pháp chung, đây có thể là những tiêu chí chung:

  • Tên tệp phải giống với chuỗi.
  • Mã hóa phải được đảo ngược nếu có thể.
  • Xác suất va chạm nên được giảm thiểu.

Để đạt được điều này, chúng ta có thể sử dụng regex để khớp các ký tự không hợp lệ, mã hóa phần trăm chúng, sau đó giới hạn độ dài của chuỗi mã hóa.

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-]");

private static final int MAX_LENGTH = 127;

public static String escapeStringAsFilename(String in){

    StringBuffer sb = new StringBuffer();

    // Apply the regex.
    Matcher m = PATTERN.matcher(in);

    while (m.find()) {

        // Convert matched character to percent-encoded.
        String replacement = "%"+Integer.toHexString(m.group().charAt(0)).toUpperCase();

        m.appendReplacement(sb,replacement);
    }
    m.appendTail(sb);

    String encoded = sb.toString();

    // Truncate the string.
    int end = Math.min(encoded.length(),MAX_LENGTH);
    return encoded.substring(0,end);
}

Hoa văn

Mẫu trên dựa trên một tập hợp con bảo toàn các ký tự được phép trong thông số POSIX .

Nếu bạn muốn cho phép ký tự dấu chấm, hãy sử dụng:

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-\\.]");

Chỉ cần cảnh giác với các chuỗi như "." và ".."

Nếu bạn muốn tránh va chạm trên các hệ thống tệp không phân biệt chữ hoa chữ thường, bạn sẽ cần phải thoát các chữ hoa:

private static final Pattern PATTERN = Pattern.compile("[^a-z0-9_\\-]");

Hoặc thoát các chữ cái thường:

private static final Pattern PATTERN = Pattern.compile("[^A-Z0-9_\\-]");

Thay vì sử dụng danh sách trắng, bạn có thể chọn đưa vào danh sách đen các ký tự dành riêng cho hệ thống tệp cụ thể của mình. EG Regex này phù hợp với hệ thống tệp FAT32:

private static final Pattern PATTERN = Pattern.compile("[%\\.\"\\*/:<>\\?\\\\\\|\\+,\\.;=\\[\\]]");

Chiều dài

Trên Android, 127 ký tự là giới hạn an toàn. Nhiều hệ thống tệp cho phép 255 ký tự.

Nếu bạn muốn giữ lại phần đuôi thay vì phần đầu của chuỗi, hãy sử dụng:

// Truncate the string.
int start = Math.max(0,encoded.length()-MAX_LENGTH);
return encoded.substring(start,encoded.length());

Giải mã

Để chuyển đổi tên tệp trở lại chuỗi ban đầu, hãy sử dụng:

URLDecoder.decode(filename, "UTF-8");

Hạn chế

Bởi vì các chuỗi dài hơn bị cắt ngắn, có khả năng xảy ra xung đột tên khi mã hóa hoặc hỏng khi giải mã.


1
Posix phép có dấu gạch nối - bạn nên thêm nó vào mô hình -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev

Đã thêm dấu gạch nối. Cảm ơn :)
SharkAlley

Tôi không nghĩ rằng phần trăm mã hóa sẽ làm việc vui lòng vào cửa sổ, cho rằng đó là một nhân vật dành riêng ..
Amalgovinus

1
Không xem xét các ngôn ngữ không phải tiếng Anh.
NateS

5

Hãy thử sử dụng regex sau để thay thế mọi ký tự tên tệp không hợp lệ bằng khoảng trắng:

public static String toValidFileName(String input)
{
    return input.replaceAll("[:\\\\/*\"?|<>']", " ");
}

Khoảng trắng khó chịu đối với CLI; xem xét thay thế bằng _hoặc -.
sdgfsdh


2

Đây có lẽ không phải là cách hiệu quả nhất, nhưng cho thấy cách thực hiện bằng cách sử dụng đường ống dẫn Java 8:

private static String sanitizeFileName(String name) {
    return name
            .chars()
            .mapToObj(i -> (char) i)
            .map(c -> Character.isWhitespace(c) ? '_' : c)
            .filter(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_')
            .map(String::valueOf)
            .collect(Collectors.joining());
}

Giải pháp có thể được cải thiện bằng cách tạo bộ sưu tập tùy chỉnh sử dụng StringBuilder, vì vậy bạn không cần phải truyền từng ký tự có trọng lượng nhẹ thành chuỗi có trọng lượng nặng.


-1

Bạn có thể xóa các ký tự không hợp lệ ('/', '\', '?', '*') Và sau đó sử dụng nó.


1
Điều này sẽ giới thiệu khả năng xung đột đặt tên. Tức là, "tes? T", "tes * t" và "test" sẽ đi cùng một tệp "test".
vog

Thật. Sau đó thay thế chúng. Ví dụ: '/' -> gạch chéo, '*' -> dấu sao ... hoặc sử dụng hàm băm như vog đề xuất.
Burkhard

4
Bạn luôn mở cửa cho khả năng xảy ra xung đột đặt tên
Brian Agnew

2
"?" và "*" là các ký tự được phép trong tên tệp. Chúng chỉ cần được thoát trong các lệnh shell, bởi vì thường sử dụng globbing. Tuy nhiên, ở cấp độ API tệp, không có vấn đề gì.
vog

2
@Brian Agnew: không thực sự đúng. Các lược đồ mã hóa các ký tự không hợp lệ bằng cách sử dụng một lược đồ thoát có thể đảo ngược sẽ không gây ra xung đột.
Stephen C
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.