Dosya adı olarak kullanmak için Java'da bir dizeyi nasıl güvenli bir şekilde kodlayabilirim?


117

Harici bir işlemden bir dize alıyorum. Bir dosya adı oluşturmak için bu String'i kullanmak ve ardından o dosyaya yazmak istiyorum. Bunu yapmak için kod pasajım:

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

S Unix tabanlı bir işletim sisteminde '/' gibi geçersiz bir karakter içeriyorsa, bir java.io.FileNotFoundException (haklı olarak) atılır.

Dosya adı olarak kullanılabilmesi için String'i nasıl güvenli bir şekilde kodlayabilirim?

Düzenleme: Umduğum şey, bunu benim için yapan bir API çağrısı.

Bunu yapabilirim:

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

Ancak URLEncoder'ın bu amaç için güvenilir olup olmadığından emin değilim.


1
Dizeyi kodlamanın amacı nedir?
Stephen C

3
@Stephen C: Dizeyi kodlamanın amacı, java.net.URLEncoder'ın URL'ler için yaptığı gibi dosya adı olarak kullanıma uygun hale getirmektir.
Steve McLeod

1
Ah anlıyorum. Kodlamanın tersine çevrilebilir olması gerekiyor mu?
Stephen C

@Stephen C: Hayır, tersine çevrilebilir olması gerekmez, ancak sonucun orijinal dizgeye olabildiğince yakın olmasını istiyorum.
Steve McLeod

1
Kodlamanın orijinal adı gizlemesi gerekiyor mu? 1'e 1 olması gerekiyor mu; yani çarpışmalar iyi mi?
Stephen C

Yanıtlar:


17

Sonucun orijinal dosyaya benzemesini istiyorsanız, SHA-1 veya başka herhangi bir karma şeması çözüm değildir. Çarpışmalardan kaçınılması gerekiyorsa, "kötü" karakterlerin basitçe değiştirilmesi veya kaldırılması da çözüm değildir.

Bunun yerine bunun gibi bir şey istiyorsunuz. (Not: Bu, kopyalayıp yapıştırılacak bir şey değil, açıklayıcı bir örnek olarak görülmelidir.)

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

Bu çözüm, kodlanmış dizelerin çoğu durumda orijinal dizelere benzediği tersine çevrilebilir bir kodlama (çarpışmadan) sağlar. 8 bitlik karakterler kullandığınızı varsayıyorum.

URLEncoder çalışır, ancak birçok yasal dosya adı karakterini kodlaması dezavantajına sahiptir.

Geri dönüşü garanti edilmeyen bir çözüm istiyorsanız, o zaman 'kötü' karakterleri kaçış dizileriyle değiştirmek yerine kaldırın.


Yukarıdaki kodlamanın tersi, uygulanması için eşit derecede düz olmalıdır.


105

Benim önerim "beyaz liste" yaklaşımını benimsemektir, yani kötü karakterleri filtrelemeye çalışmayın. Bunun yerine neyin iyi olduğunu tanımlayın. Dosya adını reddedebilir veya filtreleyebilirsiniz. Filtrelemek istiyorsanız:

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

Bunun yaptığı şey , sayı, harf veya alt çizgi olmayan herhangi bir karakteri hiçbir şey olmadan değiştirmektir. Alternatif olarak, bunları başka bir karakterle (alt çizgi gibi) değiştirebilirsiniz.

Sorun şu ki, bu paylaşılan bir dizinse, dosya adı çakışmasını istemezsiniz. Kullanıcı depolama alanları kullanıcı tarafından ayrılmış olsa bile, yalnızca kötü karakterleri filtreleyerek çakışan bir dosya adıyla karşılaşabilirsiniz. Bir kullanıcının koyduğu ad, onu da indirmek isterse, genellikle yararlıdır.

Bu nedenle, kullanıcının istediğini girmesine izin verme eğilimindeyim, dosya adını kendi seçtiğim bir şemaya göre (örneğin, userId_fileId) ve ardından kullanıcının dosya adını bir veritabanı tablosunda saklamaya izin veriyorum. Bu şekilde, kullanıcıya geri görüntüleyebilir, şeyleri istediğiniz gibi saklayabilir ve güvenlikten ödün vermez veya diğer dosyaları silebilirsiniz.

Dosyaya hashing uygulayabilirsiniz (örn. MD5 hash), ancak daha sonra kullanıcının koyduğu dosyaları listeleyemezsiniz (zaten anlamlı bir adla değil).

DÜZENLEME: Java için normal ifade düzeltildi


Önce kötü çözümü sağlamanın iyi bir fikir olduğunu düşünmüyorum. Ek olarak, MD5 neredeyse kırılmış bir hash algoritmasıdır. En azından SHA-1 veya daha iyisini tavsiye ederim.
vog

19
Benzersiz bir dosya adı oluşturmak için algoritmanın "bozuk" olup olmadığını kimin umursadığı?
cletus

3
@cletus: sorun, farklı dizelerin aynı dosya adıyla eşleşmesidir; yani çarpışma.
Stephen C

3
Bir çarpışmanın kasıtlı olması gerekirdi, asıl soru bu dizelerin bir saldırgan tarafından seçildiğinden bahsetmiyor.
tialaramex

8
"\\W+"Java'da regexp için kullanmanız gerekir . Ters eğik çizgi ilk olarak dizenin kendisi \Wiçin geçerlidir ve geçerli bir kaçış dizisi değildir. Cevabı düzenlemeyi denedim, ancak görünüşe göre birisi düzenlememi reddetti :(
vadipp

35

Bu, kodlamanın tersine çevrilebilir olup olmamasına bağlıdır.

çevrilebilir

java.net.URLEncoderÖzel karakterleri ile değiştirmek için URL kodlamasını ( ) kullanın %xx. Dizenin eşit olduğu , eşit olduğu veya boş olduğu özel durumlara dikkat ettiğinize dikkat edin ! ¹ Birçok program dosya adları oluşturmak için URL kodlaması kullanır, bu nedenle bu herkesin anladığı standart bir tekniktir....

dönülemez

Verilen dizenin karmasını (ör. SHA-1) kullanın. Modern karma algoritmalar ( MD5 değil ) çarpışmasız olarak kabul edilebilir. Aslında, bir çarpışma bulursanız kriptografide bir kırılma yaşarsınız.


¹ 3 özel durumu da gibi bir önek kullanarak zarif bir şekilde halledebilirsiniz "myApp-". Dosyayı doğrudan içine koyarsanız, $HOME".bashrc" gibi mevcut dosyalarla çakışmaları önlemek için bunu yine de yapmanız gerekir.
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'ın özel karakterin ne olduğuna dair fikri doğru olmayabilir.
Stephen C

4
@vog: URLEncoder "." için başarısız oluyor. ve "..". Bunlar kodlanmalıdır, yoksa $ HOME'daki dizin girişleriyle çarpışırsınız
Stephen C

6
@vog: "*" yalnızca Unix tabanlı çoğu dosya sisteminde izin verilir, NTFS ve FAT32 bunu desteklemez.
Jonathan

1
"" ve ".." dize yalnızca nokta olduğunda noktalar% 2E'ye kaçarak ele alınabilir (eğer kaçış dizilerini küçültmek istiyorsanız). '*', "% 2A" ile de değiştirilebilir.
viphe

1
dosya adını uzatan herhangi bir yaklaşımın (tek karakterleri% 20 veya herhangi bir şekilde değiştirerek) uzunluk sınırına yakın olan bazı dosya adlarını geçersiz kılacağını unutmayın (Unix sistemleri için 255 karakter)
smcg

24

İşte kullandığım şey:

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

Bunun yaptığı şey, normal ifade kullanarak harf, sayı, alt çizgi veya nokta olmayan her karakteri alt çizgi ile değiştirmektir.

Bu, "TL'yi $ 'a dönüştürme" gibi bir şeyin "How_to_convert___to__" olacağı anlamına gelir. Kuşkusuz, bu sonuç çok kullanıcı dostu değildir, ancak güvenlidir ve sonuçta ortaya çıkan dizin / dosya adlarının her yerde çalışması garanti edilir. Benim durumumda, sonuç kullanıcıya gösterilmiyor ve bu nedenle bir sorun değil, ancak normal ifadeyi daha izin verici olacak şekilde değiştirmek isteyebilirsiniz.

Karşılaştığım başka bir sorunun da bazen aynı isimleri almamdı (kullanıcı girdisine dayandığından), bu nedenle tek bir dizinde aynı ada sahip birden fazla dizin / dosya bulunamayacağından bunun farkında olmalısınız. . Sadece geçerli saati ve tarihi ve bundan kaçınmak için kısa bir rastgele dize ekledim. (aynı dosya adları aynı karmalarla sonuçlanacağından dosya adının karması değil gerçek bir rastgele dize)

Ayrıca, bazı sistemlerin sahip olduğu 255 karakter sınırını aşabileceğinden, ortaya çıkan dizeyi kısaltmanız veya başka şekilde kısaltmanız gerekebilir.


6
Diğer bir sorun da ASCII karakterlerini kullanan dillere özgü olmasıdır. Diğer diller için, alt çizgiden başka hiçbir şey içermeyen dosya adlarıyla sonuçlanır.
Andy Thomas

13

Genel bir çözüm arayanlar için bunlar ortak kriterler olabilir:

  • Dosya adı dizeye benzemelidir.
  • Kodlama, mümkün olduğunda tersine çevrilebilir olmalıdır.
  • Çarpışma olasılığı en aza indirilmelidir.

Bunu başarmak için, kural dışı karakterleri eşleştirmek, bunları yüzde olarak kodlamak ve ardından kodlanmış dizenin uzunluğunu sınırlamak için regex kullanabiliriz .

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

desenler

Yukarıdaki model , POSIX spesifikasyonunda izin verilen karakterlerin muhafazakar bir alt kümesine dayanmaktadır .

Nokta karakterine izin vermek istiyorsanız, şunu kullanın:

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

"" Gibi dizelere karşı dikkatli olun. ve ".."

Büyük / küçük harfe duyarlı olmayan dosya sistemlerinde çarpışmaları önlemek istiyorsanız, büyük harflerden kaçmanız gerekir:

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

Veya küçük harflerden kaçının:

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

Beyaz liste kullanmak yerine, belirli dosya sisteminiz için ayrılmış karakterleri kara listeye almayı seçebilirsiniz. EG Bu normal ifade FAT32 dosya sistemlerine uygundur:

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

uzunluk

Android'de güvenli sınır 127 karakterdir . Çoğu dosya sistemi 255 karaktere izin verir.

İpinizin başı yerine kuyruğu tutmayı tercih ediyorsanız, şunu kullanın:

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

Decoding

Dosya adını tekrar orijinal dizeye dönüştürmek için şunu kullanın:

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

Sınırlamalar

Daha uzun dizeler kesildiği için, kodlama sırasında ad çakışması veya kod çözme sırasında bozulma olasılığı vardır.


1
Posix kısa çizgilere izin verir - bunu desene eklemelisiniz -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev

Kısa çizgiler eklendi. Teşekkürler :)
SharkAlley

Ayrılmış bir karakter olduğu göz önüne alındığında, yüzde kodlamanın pencerelerde iyi çalışacağını sanmıyorum ..
Amalgovinus

1
İngilizce olmayan dilleri dikkate almıyor.
kabaetler

5

Her geçersiz dosya adı karakterini bir boşlukla değiştiren aşağıdaki normal ifadeyi kullanmayı deneyin:

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

Alanlar CLI'ler için kötüdür; _veya ile değiştirmeyi düşünün -.
sdgfsdh


2

Bu muhtemelen en etkili yol değildir, ancak Java 8 ardışık düzenlerini kullanarak nasıl yapılacağını gösterir:

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

Çözüm, StringBuilder kullanan özel bir toplayıcı oluşturarak geliştirilebilir, böylece her bir hafif karakteri ağır bir dizeye dönüştürmek zorunda kalmazsınız.


-1

Geçersiz karakterleri ('/', '\', '?', '*') Kaldırabilir ve sonra kullanabilirsiniz.


1
Bu, çatışmaları adlandırma olasılığını ortaya çıkaracaktır. Yani, "tes? T", "tes * t" ve "test" aynı "test" dosyasına gider.
vog

Doğru. Sonra onları değiştirin. Örneğin, '/' -> eğik çizgi, '*' -> yıldız ... veya önerildiği gibi bir karma kullanın.
Burkhard

4
Sen hep adlandırma çatışmalar olasılığına açık
Brian Agnew

2
"?" ve "*" dosya adlarında izin verilen karakterlerdir. Sadece kabuk komutlarında kaçmaları gerekir, çünkü genellikle globbing kullanılır. Dosya API seviyesinde ise sorun yok.
vog

2
@ Brian Agnew: Aslında doğru değil. Tersine çevrilebilir kaçış şeması kullanarak geçersiz karakterleri kodlayan şemalar, çakışmalara neden olmaz.
Stephen C
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.