Migrate ExoCache CacheSpan filenames from v1 to v2

V2 supports encoding special characters while on disk.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117228319
This commit is contained in:
olly 2016-03-15 05:20:39 -07:00 committed by Oliver Woodman
parent 1ca32cced8
commit 0135aaa122
4 changed files with 142 additions and 8 deletions

View file

@ -161,4 +161,20 @@ public class UtilTest extends TestCase {
assertEquals(value, reconstructedValue);
}
public void testUnescapeInvalidFileName() {
assertNull(Util.unescapeFileName("%a"));
assertNull(Util.unescapeFileName("%xyz"));
}
public void testEscapeUnescapeFileName() {
assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName");
assertEscapeUnescapeFileName("key:value", "key%3avalue");
assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25");
}
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertEquals(escapedFileName, Util.escapeFileName(fileName));
assertEquals(fileName, Util.unescapeFileName(escapedFileName));
}
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.upstream.cache;
import com.google.android.exoplayer.util.Util;
import java.io.File;
import java.util.regex.Matcher;
@ -25,10 +26,11 @@ import java.util.regex.Pattern;
*/
public final class CacheSpan implements Comparable<CacheSpan> {
private static final String SUFFIX = ".v1.exo";
private static final String SUFFIX_ESCAPED = "\\.v1\\.exo";
private static final Pattern cacheFilePattern =
Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)(" + SUFFIX_ESCAPED + ")$");
private static final String SUFFIX = ".v2.exo";
private static final Pattern CACHE_FILE_PATTERN_V1 =
Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$");
private static final Pattern CACHE_FILE_PATTERN_V2 =
Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$");
/**
* The cache key that uniquely identifies the original stream.
@ -57,7 +59,8 @@ public final class CacheSpan implements Comparable<CacheSpan> {
public static File getCacheFileName(File cacheDir, String key, long offset,
long lastAccessTimestamp) {
return new File(cacheDir, key + "." + offset + "." + lastAccessTimestamp + SUFFIX);
return new File(cacheDir,
Util.escapeFileName(key) + "." + offset + "." + lastAccessTimestamp + SUFFIX);
}
public static CacheSpan createLookup(String key, long position) {
@ -79,12 +82,25 @@ public final class CacheSpan implements Comparable<CacheSpan> {
* @return The span, or null if the file name is not correctly formatted.
*/
public static CacheSpan createCacheEntry(File file) {
Matcher matcher = cacheFilePattern.matcher(file.getName());
Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(file.getName());
if (!matcher.matches()) {
return null;
}
return CacheSpan.createCacheEntry(matcher.group(1), Long.parseLong(matcher.group(2)),
Long.parseLong(matcher.group(3)), file);
String key = Util.unescapeFileName(matcher.group(1));
return key == null ? null : createCacheEntry(
key, Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)), file);
}
static File upgradeIfNeeded(File file) {
Matcher matcher = CACHE_FILE_PATTERN_V1.matcher(file.getName());
if (!matcher.matches()) {
return file;
}
String key = matcher.group(1); // Keys were not escaped in version 1.
File newCacheFile = getCacheFileName(file.getParentFile(), key,
Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)));
file.renameTo(newCacheFile);
return newCacheFile;
}
private static CacheSpan createCacheEntry(String key, long position, long lastAccessTimestamp,

View file

@ -245,6 +245,7 @@ public final class SimpleCache implements Cache {
if (file.length() == 0) {
file.delete();
} else {
file = CacheSpan.upgradeIfNeeded(file);
CacheSpan span = CacheSpan.createCacheEntry(file);
if (span == null) {
file.delete();

View file

@ -114,6 +114,8 @@ public final class Util {
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
private static final long MAX_BYTES_TO_DRAIN = 2048;
private Util() {}
@ -786,4 +788,103 @@ public final class Util {
return TYPE_OTHER;
}
}
/**
* Escapes a string so that it's safe for use as a file or directory name on at least FAT32
* filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
*
* <p>For simplicity, this only handles common characters known to be illegal on FAT32:
* <, >, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape character.
* Escaping is performed in a consistent way so that no collisions occur and
* {@link #unescapeFileName(String)} can be used to retrieve the original file name.
*
* @param fileName File name to be escaped.
* @return An escaped file name which will be safe for use on at least FAT32 filesystems.
*/
public static String escapeFileName(String fileName) {
int length = fileName.length();
int charactersToEscapeCount = 0;
for (int i = 0; i < length; i++) {
if (shouldEscapeCharacter(fileName.charAt(i))) {
charactersToEscapeCount++;
}
}
if (charactersToEscapeCount == 0) {
return fileName;
}
int i = 0;
StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);
while (charactersToEscapeCount > 0) {
char c = fileName.charAt(i++);
if (shouldEscapeCharacter(c)) {
builder.append('%').append(Integer.toHexString(c));
charactersToEscapeCount--;
} else {
builder.append(c);
}
}
if (i < length) {
builder.append(fileName, i, length);
}
return builder.toString();
}
private static boolean shouldEscapeCharacter(char c) {
switch (c) {
case '<':
case '>':
case ':':
case '"':
case '/':
case '\\':
case '|':
case '?':
case '*':
case '%':
return true;
default:
return false;
}
}
/**
* Unescapes an escaped file or directory name back to its original value.
*
* <p>See {@link #escapeFileName(String)} for more information.
*
* @param fileName File name to be unescaped.
* @return The original value of the file name before it was escaped,
* or null if the escaped fileName seems invalid.
*/
public static String unescapeFileName(String fileName) {
int length = fileName.length();
int percentCharacterCount = 0;
for (int i = 0; i < length; i++) {
if (fileName.charAt(i) == '%') {
percentCharacterCount++;
}
}
if (percentCharacterCount == 0) {
return fileName;
}
int expectedLength = length - percentCharacterCount * 2;
StringBuilder builder = new StringBuilder(expectedLength);
Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
int endOfLastMatch = 0;
while (percentCharacterCount > 0 && matcher.find()) {
char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
endOfLastMatch = matcher.end();
percentCharacterCount--;
}
if (endOfLastMatch < length) {
builder.append(fileName, endOfLastMatch, length);
}
if (builder.length() != expectedLength) {
return null;
}
return builder.toString();
}
}