ฉันจะเข้ารหัสสตริงใน Java อย่างปลอดภัยเพื่อใช้เป็นชื่อไฟล์ได้อย่างไร


117

ฉันได้รับสตริงจากกระบวนการภายนอก ฉันต้องการใช้ String นั้นเพื่อสร้างชื่อไฟล์จากนั้นเขียนลงในไฟล์นั้น นี่คือข้อมูลโค้ดของฉันที่จะทำสิ่งนี้:

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

ถ้า s มีอักขระที่ไม่ถูกต้องเช่น '/' ในระบบปฏิบัติการที่ใช้ Unix ดังนั้น java.io.FileNotFoundException จะถูกโยนทิ้ง (อย่างถูกต้อง)

ฉันจะเข้ารหัส String อย่างปลอดภัยเพื่อให้สามารถใช้เป็นชื่อไฟล์ได้อย่างไร

แก้ไข: สิ่งที่ฉันหวังคือการเรียก API ที่ทำเพื่อฉัน

ฉันสามารถทำได้:

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

แต่ฉันไม่แน่ใจว่า URLEncoder เชื่อถือได้สำหรับวัตถุประสงค์นี้หรือไม่


1
วัตถุประสงค์ของการเข้ารหัสสตริงคืออะไร?
Stephen C

3
@Stephen C: วัตถุประสงค์ของการเข้ารหัสสตริงคือเพื่อให้เหมาะสำหรับใช้เป็นชื่อไฟล์เช่นเดียวกับที่ java.net.URLEncoder ทำสำหรับ URL
Steve McLeod

1
อ้อเข้าใจแล้ว. การเข้ารหัสจำเป็นต้องย้อนกลับได้หรือไม่?
Stephen C

@Stephen C: ไม่ไม่จำเป็นต้องย้อนกลับได้ แต่ฉันต้องการให้ผลลัพธ์ใกล้เคียงกับสตริงเดิมมากที่สุด
Steve McLeod

1
การเข้ารหัสจำเป็นต้องปิดบังชื่อเดิมหรือไม่? จำเป็นต้องเป็น 1 ต่อ 1 หรือไม่ เช่นการชนกันตกลงหรือไม่?
Stephen C

คำตอบ:


17

หากคุณต้องการให้ผลลัพธ์คล้ายกับไฟล์ต้นฉบับ SHA-1 หรือโครงร่างการแฮชอื่น ๆ ไม่ใช่คำตอบ หากต้องหลีกเลี่ยงการชนกันการแทนที่หรือลบอักขระ "ไม่ดี" อย่างง่าย ๆ ก็ไม่ใช่คำตอบเช่นกัน

แต่คุณต้องการอะไรแบบนี้ (หมายเหตุ: ควรถือเป็นตัวอย่างประกอบไม่ใช่สิ่งที่จะคัดลอกและวาง)

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);

โซลูชันนี้ให้การเข้ารหัสแบบย้อนกลับได้ (โดยไม่มีการชนกัน) โดยที่สตริงที่เข้ารหัสมีลักษณะคล้ายกับสตริงดั้งเดิมในกรณีส่วนใหญ่ ฉันสมมติว่าคุณใช้อักขระ 8 บิต

URLEncoder ใช้งานได้ แต่มีข้อเสียตรงที่เข้ารหัสอักขระชื่อไฟล์ทางกฎหมายจำนวนมาก

หากคุณต้องการวิธีแก้ปัญหาที่ไม่รับประกันว่าจะย้อนกลับได้ให้ลบอักขระ 'bad' ออกแทนที่จะแทนที่ด้วยลำดับ Escape


การย้อนกลับของการเข้ารหัสข้างต้นควรตรงไปตรงมาอย่างเท่าเทียมกันในการใช้งาน


105

คำแนะนำของฉันคือใช้แนวทาง "บัญชีขาว" หมายความว่าอย่าพยายามกรองตัวละครที่ไม่ดีออกไป ให้กำหนดสิ่งที่ตกลงแทน คุณสามารถปฏิเสธชื่อไฟล์หรือกรองได้ หากคุณต้องการกรอง:

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

สิ่งนี้จะแทนที่อักขระใด ๆ ที่ไม่ใช่ตัวเลขตัวอักษรหรือขีดล่างโดยไม่มีอะไรเลย หรือคุณสามารถแทนที่ด้วยอักขระอื่น (เช่นขีดล่าง)

ปัญหาคือถ้านี่เป็นไดเร็กทอรีที่ใช้ร่วมกันคุณไม่ต้องการให้ชื่อไฟล์ชนกัน แม้ว่าพื้นที่จัดเก็บข้อมูลของผู้ใช้จะถูกแยกโดยผู้ใช้ แต่คุณอาจพบชื่อไฟล์ที่ชนกันเพียงแค่กรองอักขระที่ไม่ถูกต้องออกไป ชื่อที่ผู้ใช้ใส่มักจะมีประโยชน์หากพวกเขาต้องการดาวน์โหลดด้วย

ด้วยเหตุนี้ฉันมักจะอนุญาตให้ผู้ใช้ป้อนสิ่งที่ต้องการเก็บชื่อไฟล์ตามรูปแบบที่ฉันเลือกเอง (เช่น userId_fileId) จากนั้นเก็บชื่อไฟล์ของผู้ใช้ในตารางฐานข้อมูล ด้วยวิธีนี้คุณสามารถแสดงกลับไปยังผู้ใช้จัดเก็บสิ่งต่างๆตามที่คุณต้องการและคุณไม่ลดทอนความปลอดภัยหรือล้างไฟล์อื่น ๆ

คุณยังสามารถแฮชไฟล์ได้ (เช่นแฮช MD5) แต่คุณจะไม่สามารถแสดงรายการไฟล์ที่ผู้ใช้ใส่เข้าไปได้ (ไม่ใช่ชื่อที่มีความหมายอยู่ดี)

แก้ไข: แก้ไข regex สำหรับ java


ฉันไม่คิดว่าเป็นความคิดที่ดีที่จะหาทางออกที่ไม่ดีก่อน นอกจากนี้ MD5 ยังเป็นอัลกอริทึมแฮชที่เกือบจะแตก ฉันแนะนำอย่างน้อย SHA-1 หรือดีกว่า
vog

19
สำหรับวัตถุประสงค์ในการสร้างชื่อไฟล์เฉพาะใครสนใจว่าอัลกอริทึม "เสีย"?
cletus

3
@cletus: ปัญหาคือสตริงที่แตกต่างกันจะแมปกับชื่อไฟล์เดียวกัน เช่นการชนกัน
Stephen C

3
การปะทะกันจะต้องพิจารณาโดยเจตนาคำถามเดิมไม่ได้พูดถึงสตริงเหล่านี้ที่ผู้โจมตีเลือก
tialaramex

8
คุณต้องใช้"\\W+"สำหรับ regexp ใน Java ก่อนอื่นแบ็กสแลชจะใช้กับสตริงเองและ\Wไม่ใช่ลำดับการหลีกเลี่ยงที่ถูกต้อง ฉันพยายามแก้ไขคำตอบ แต่ดูเหมือนว่ามีคนปฏิเสธการแก้ไขของฉัน :(
vadipp

35

ขึ้นอยู่กับว่าการเข้ารหัสควรย้อนกลับได้หรือไม่

กลับได้

ใช้การเข้ารหัส URL ( java.net.URLEncoder) %xxแทนตัวอักษรพิเศษกับ โปรดทราบว่าคุณดูแลกรณีพิเศษที่สตริงเท่ากับ.เท่ากับ..หรือว่าง! ¹หลายโปรแกรมใช้การเข้ารหัส URL เพื่อสร้างชื่อไฟล์ดังนั้นนี่จึงเป็นเทคนิคมาตรฐานที่ทุกคนเข้าใจ

กลับไม่ได้

ใช้แฮช (เช่น SHA-1) ของสตริงที่กำหนด อัลกอริทึมแฮชสมัยใหม่ ( ไม่ใช่ MD5) ถือได้ว่าไม่มีการชนกัน ในความเป็นจริงคุณจะมีช่องโหว่ในการเข้ารหัสหากคุณพบการปะทะกัน


¹คุณสามารถจัดการกรณีพิเศษทั้ง 3 กรณีได้อย่างหรูหราโดยใช้คำนำหน้าเช่น"myApp-". หากคุณใส่ไฟล์ลงในโดยตรง$HOMEคุณจะต้องทำเช่นนั้นเพื่อหลีกเลี่ยงความขัดแย้งกับไฟล์ที่มีอยู่เช่น ".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
ความคิดของ URLEncoder เกี่ยวกับอักขระพิเศษอาจไม่ถูกต้อง
Stephen C

4
@vog: URLEncoder ล้มเหลวสำหรับ "." และ ".. ". สิ่งเหล่านี้ต้องเข้ารหัสมิฉะนั้นคุณจะชนกับรายการไดเรกทอรีใน $ HOME
Stephen C

6
@vog: "*" ได้รับอนุญาตในระบบไฟล์ที่ใช้ Unix ส่วนใหญ่เท่านั้น NTFS และ FAT32 ไม่รองรับ
Jonathan

1
"" และ ".. " สามารถจัดการได้โดยการหลีกเลี่ยงจุดไปยัง% 2E เมื่อสตริงเป็นเพียงจุด (หากคุณต้องการลดลำดับการหลีกเลี่ยง) "*" สามารถแทนที่ด้วย "% 2A" ได้เช่นกัน
viphe

1
โปรดทราบว่าวิธีการใด ๆ ที่ทำให้ชื่อไฟล์ยาวขึ้น (โดยการเปลี่ยนอักขระเดี่ยวเป็น% 20 หรืออะไรก็ตาม) จะทำให้ชื่อไฟล์บางไฟล์ที่มีความยาวใกล้ถึงขีดจำกัดความยาว (255 ตัวอักษรสำหรับระบบ Unix)
smcg

24

นี่คือสิ่งที่ฉันใช้:

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

สิ่งนี้คือแทนที่อักขระทุกตัวที่ไม่ใช่ตัวอักษรตัวเลขขีดล่างหรือจุดด้วยขีดล่างโดยใช้ regex

ซึ่งหมายความว่า "วิธีการแปลง£เป็น $" จะกลายเป็น "How_to_convert___to__" เป็นที่ยอมรับว่าผลลัพธ์นี้ไม่เป็นมิตรกับผู้ใช้มากนัก แต่ปลอดภัยและรับประกันว่าไดเร็กทอรี / ชื่อไฟล์ที่เป็นผลลัพธ์จะทำงานได้ทุกที่ ในกรณีของฉันผลลัพธ์จะไม่แสดงให้ผู้ใช้เห็นดังนั้นจึงไม่ใช่ปัญหา แต่คุณอาจต้องการแก้ไขนิพจน์ทั่วไปเพื่อให้อนุญาตมากขึ้น

ที่น่าสังเกตว่าปัญหาอื่นที่ฉันพบคือบางครั้งฉันจะได้รับชื่อที่เหมือนกัน (เนื่องจากขึ้นอยู่กับการป้อนข้อมูลของผู้ใช้) ดังนั้นคุณควรตระหนักถึงเรื่องนี้เนื่องจากคุณไม่สามารถมีหลายไดเรกทอรี / ไฟล์ที่มีชื่อเดียวกันในไดเรกทอรีเดียว . ฉันแค่ใส่เวลาและวันที่ปัจจุบันไว้ข้างหน้าและสตริงสุ่มสั้น ๆ เพื่อหลีกเลี่ยงสิ่งนั้น (สตริงสุ่มจริงไม่ใช่แฮชของชื่อไฟล์เนื่องจากชื่อไฟล์ที่เหมือนกันจะส่งผลให้แฮชเหมือนกัน)

นอกจากนี้คุณอาจต้องตัดทอนหรือย่อสตริงผลลัพธ์เนื่องจากอาจเกินขีด จำกัด 255 อักขระที่ระบบบางระบบมี


6
ปัญหาอีกประการหนึ่งคือเป็นปัญหาเฉพาะสำหรับภาษาที่ใช้อักขระ ASCII สำหรับภาษาอื่น ๆ จะส่งผลให้ชื่อไฟล์ไม่มีอะไรเลยนอกจากขีดล่าง
Andy Thomas

13

สำหรับผู้ที่มองหาวิธีแก้ปัญหาทั่วไปสิ่งเหล่านี้อาจเป็น critera ทั่วไป:

  • ชื่อไฟล์ควรคล้ายกับสตริง
  • การเข้ารหัสควรย้อนกลับได้หากทำได้
  • ควรลดความน่าจะเป็นของการชนให้น้อยที่สุด

เพื่อให้บรรลุสิ่งนี้เราสามารถใช้ regex เพื่อจับคู่อักขระที่ผิดกฎหมายเข้ารหัสเปอร์เซ็นต์เหล่านั้นจากนั้น จำกัด ความยาวของสตริงที่เข้ารหัส

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

รูปแบบ

รูปแบบดังกล่าวข้างต้นจะขึ้นอยู่กับเซตอนุลักษณ์ของตัวละครที่ได้รับอนุญาตในข้อมูลจำเพาะ POSIX

หากคุณต้องการอนุญาตให้ใช้อักขระจุด:

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

ระวังสตริงเช่น "." และ ".. "

หากคุณต้องการหลีกเลี่ยงการชนกันในระบบไฟล์ที่ไม่คำนึงถึงตัวพิมพ์เล็กและใหญ่คุณจะต้องหลีกเลี่ยงตัวพิมพ์ใหญ่:

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

หรือหลีกหนีอักษรตัวพิมพ์เล็ก:

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

แทนที่จะใช้รายการที่อนุญาตคุณอาจเลือกที่จะขึ้นบัญชีดำอักขระที่สงวนไว้สำหรับระบบไฟล์เฉพาะของคุณ EG regex นี้เหมาะกับระบบไฟล์ FAT32:

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

ความยาว

บน Android 127 อักขระคือขีด จำกัด ที่ปลอดภัย ระบบไฟล์จำนวนมากอนุญาต 255 อักขระ

หากคุณต้องการคงหางไว้แทนที่จะเป็นส่วนหัวของสตริงให้ใช้:

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

ถอดรหัส

ในการแปลงชื่อไฟล์กลับเป็นสตริงเดิมให้ใช้:

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

ข้อ จำกัด

เนื่องจากสตริงที่ยาวกว่าถูกตัดทอนจึงมีความเป็นไปได้ที่ชื่อจะชนกันเมื่อเข้ารหัสหรือเกิดความเสียหายเมื่อถอดรหัส


1
Posix อนุญาตยัติภังค์ - คุณควรเพิ่มลงในรูปแบบ -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev

เพิ่มยัติภังค์แล้ว ขอบคุณ :)
SharkAlley

ฉันไม่คิดว่าการเข้ารหัสเปอร์เซ็นต์จะทำงานได้ดีบน windows เนื่องจากเป็นอักขระที่สงวนไว้ ..
Amalgovinus

1
ไม่พิจารณาภาษาที่ไม่ใช่ภาษาอังกฤษ
NateS

5

ลองใช้ regex ต่อไปนี้ซึ่งแทนที่อักขระชื่อไฟล์ที่ไม่ถูกต้องทุกตัวด้วยช่องว่าง:

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

ช่องว่างเป็นสิ่งที่น่ารังเกียจสำหรับ CLI พิจารณาเปลี่ยนด้วยหรือ_ -
sdgfsdh


2

นี่อาจไม่ใช่วิธีที่มีประสิทธิภาพมากที่สุด แต่แสดงวิธีการใช้ Java 8 pipelines:

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());
}

โซลูชันนี้สามารถปรับปรุงได้โดยการสร้างตัวรวบรวมแบบกำหนดเองซึ่งใช้ StringBuilder ดังนั้นคุณไม่จำเป็นต้องโยนอักขระที่มีน้ำหนักเบาแต่ละตัวให้เป็นสตริงที่มีน้ำหนักมาก


-1

คุณสามารถลบตัวอักษรที่ไม่ถูกต้อง ('/', '\', '?', '*') แล้วใช้งานได้


1
สิ่งนี้จะแนะนำความเป็นไปได้ของการตั้งชื่อที่ขัดแย้งกัน เช่น "tes? t", "tes * t" และ "test" จะไปในไฟล์ "test" เดียวกัน
vog

จริง จากนั้นแทนที่พวกเขา ตัวอย่างเช่น '/' -> slash, '*' -> star ... หรือใช้แฮชตามที่ vog แนะนำ
Burkhard

4
คุณมักจะเปิดให้เป็นไปได้ของความขัดแย้งในการตั้งชื่อ
ไบรอัน Agnew

2
"?" และ "*" เป็นอักขระที่อนุญาตในชื่อไฟล์ พวกเขาจำเป็นต้องหลบหนีในคำสั่งเชลล์เท่านั้นเพราะโดยปกติจะใช้ globbing อย่างไรก็ตามในระดับไฟล์ API ไม่มีปัญหา
vog

2
@Brian Agnew: ไม่จริง แบบแผนที่เข้ารหัสอักขระที่ไม่ถูกต้องโดยใช้รูปแบบการหลีกเลี่ยงที่ย้อนกลับได้จะไม่ทำให้เกิดการชนกัน
Stephen C
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.