Skip to content

Commit

Permalink
If serverUrl is too long (>255 characters), MD5 of the URL is used in…
Browse files Browse the repository at this point in the history
…stead (#100)

* If serverUrl is too long (>256 characters for path, >255 for file name), MD5 of the URL is used instead.
  • Loading branch information
Kenneth Geisshirt authored Sep 19, 2016
1 parent fa938b8 commit 7526eaa
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@

import io.realm.rule.RunInLooperThread;
import io.realm.rule.TestRealmConfigurationFactory;
import io.realm.util.SyncTestUtils;

import static io.realm.util.SyncTestUtils.createTestUser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
Expand Down Expand Up @@ -74,7 +76,7 @@ public void user_invalidUserThrows() {
} catch (IllegalArgumentException ignore) {
}

User user = SyncTestUtils.createTestUser(0); // Create user that has expired credentials
User user = createTestUser(0); // Create user that has expired credentials
try {
builder.user(user);
} catch (IllegalArgumentException ignore) {
Expand All @@ -83,7 +85,7 @@ public void user_invalidUserThrows() {

@Test
public void serverUrl_setsFolderAndFileName() {
User user = SyncTestUtils.createTestUser();
User user = createTestUser();
String[][] validUrls = {
// <URL>, <Folder>, <FileName>
{ "realm://objectserver.realm.io/~/default", "realm-object-server/" + user.getIdentity(), "default" },
Expand Down Expand Up @@ -127,6 +129,39 @@ public void serverUrl_invalidUrlThrows() {
}
}

private String makeServerUrl(int len) {
StringBuilder builder = new StringBuilder("realm://objectserver.realm.io/~/");
for (int i = 0; i < len; i++) {
builder.append('A');
}
return builder.toString();
}

@Test
public void serverUrl_length() {
int[] lengths = {1, SyncConfiguration.MAX_FILE_NAME_LENGTH - 1,
SyncConfiguration.MAX_FILE_NAME_LENGTH, SyncConfiguration.MAX_FILE_NAME_LENGTH + 1, 1000};

for (int len : lengths) {
SyncConfiguration.Builder builder = new SyncConfiguration.Builder(context)
.serverUrl(makeServerUrl(len))
.user(createTestUser());

SyncConfiguration config = builder.build();
assertTrue("Length: " + len, config.getRealmFileName().length() <= SyncConfiguration.MAX_FILE_NAME_LENGTH);
assertTrue("Length: " + len, config.getPath().length() <= SyncConfiguration.MAX_FULL_PATH_LENGTH);
}
}

@Test
public void serverUrl_invalidChars() {
SyncConfiguration.Builder builder = new SyncConfiguration.Builder(context)
.serverUrl("realm://objectserver.realm.io/~/?")
.user(createTestUser());
SyncConfiguration config = builder.build();
assertFalse(config.getRealmFileName().contains("?"));
}

@Test
public void userAndServerUrlRequired() {
SyncConfiguration.Builder builder;
Expand All @@ -140,7 +175,7 @@ public void userAndServerUrlRequired() {

builder = new SyncConfiguration.Builder(context);
try {
builder.user(SyncTestUtils.createTestUser(Long.MAX_VALUE)).build();
builder.user(createTestUser(Long.MAX_VALUE)).build();
} catch (IllegalStateException ignore) {
}

Expand All @@ -156,7 +191,7 @@ public void userAndServerUrlRequired() {
public void errorHandler() {
SyncConfiguration.Builder builder;
builder = new SyncConfiguration.Builder(context)
.user(SyncTestUtils.createTestUser())
.user(createTestUser())
.serverUrl("realm://objectserver.realm.io/default");

Session.ErrorHandler errorHandler = new Session.ErrorHandler() {
Expand All @@ -183,7 +218,7 @@ public void onError(Session session, ObjectServerError error) {

// Create configuration using the default handler
SyncConfiguration config = new SyncConfiguration.Builder(context)
.user(SyncTestUtils.createTestUser())
.user(createTestUser())
.serverUrl("realm://objectserver.realm.io/default")
.build();
assertEquals(errorHandler, config.getErrorHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
import android.content.Context;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;

import io.realm.annotations.RealmModule;
import io.realm.exceptions.RealmException;
import io.realm.internal.RealmProxyMediator;
import io.realm.internal.SharedRealm;
import io.realm.internal.syncpolicy.AutomaticSyncPolicy;
Expand Down Expand Up @@ -64,6 +68,12 @@
*/
public final class SyncConfiguration extends RealmConfiguration {

// The FAT file system has limitations of length. Also, not all characters are permitted.
// https://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
public static final int MAX_FULL_PATH_LENGTH = 256;
public static final int MAX_FILE_NAME_LENGTH = 255;
private static final char[] INVALID_CHARS = {'<', '>', ':', '"', '/', '\\', '|', '?', '*'};

private final URI serverUrl;
private final User user;
private final SyncPolicy syncPolicy;
Expand Down Expand Up @@ -380,6 +390,11 @@ public Builder inMemory() {
*
* This behaviour can be overwritten using {@link #name(String)} and {@link #directory(File)}.
*
* Many Android devices are using FAT32 file systems. FAT32 file systems have a limitation that
* file name cannot be longer than 255 characters. Moreover, the entire URL should not exceed 256 characters.
* If file name and underlying path are too long to handle for FAT32, a shorter unique name will be generated.
* See also @{link https://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx}.
*
* @param url URL identifying the Realm.
* @throws IllegalArgumentException if the URL is not valid.
*/
Expand Down Expand Up @@ -459,6 +474,22 @@ public Builder errorHandler(Session.ErrorHandler errorHandler) {
return this;
}

private String MD5(String in) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buf = digest.digest(in.getBytes("UTF-8"));
StringBuilder builder = new StringBuilder();
for (byte b : buf) {
builder.append(String.format("%02X", b));
}
return builder.toString();
} catch (NoSuchAlgorithmException e) {
throw new RealmException(e.getMessage());
} catch (UnsupportedEncodingException e) {
throw new RealmException(e.getMessage());
}
}

/**
* Setting this will cause the local Realm file used to synchronize changes to be deleted if the {@link User}
* defined by {@link #user(User)} logs out from the device using {@link User#logout()}.
Expand Down Expand Up @@ -495,11 +526,39 @@ public SyncConfiguration build() {
File rootDir = overrideDefaultFolder ? directory : defaultFolder;
String realmPathFromRootDir = getServerPath(resolvedServerUrl);
File realmFileDirectory = new File(rootDir, realmPathFromRootDir);

String realmFileName = overrideDefaultLocalFileName ? fileName : defaultLocalFileName;
String fullPathName = realmFileDirectory.getAbsolutePath() + File.pathSeparator + realmFileName;
// full path must not exceed 256 characters (on FAT)
if (fullPathName.length() > MAX_FULL_PATH_LENGTH) {
// path is too long, so we make the file name shorter
realmFileName = MD5(realmFileName);
fullPathName = realmFileDirectory.getAbsolutePath() + File.pathSeparator + realmFileName;
if (fullPathName.length() > MAX_FULL_PATH_LENGTH) {
// use rootDir/userIdentify as directory instead as it is shorter
realmFileDirectory = new File(rootDir, user.getIdentity());
fullPathName = realmFileDirectory.getAbsolutePath() + File.pathSeparator + realmFileName;
if (fullPathName.length() > MAX_FULL_PATH_LENGTH) { // we are out of ideas
throw new IllegalStateException(String.format("Full path name must not exceed %d characters: %s",
MAX_FULL_PATH_LENGTH, fullPathName));
}
}
}

if (realmFileName.length() > MAX_FILE_NAME_LENGTH) {
throw new IllegalStateException(String.format("File name exceed %d characters: %d", MAX_FILE_NAME_LENGTH,
realmFileName.length()));
}

// substitute invalid characters
for (char c : INVALID_CHARS) {
realmFileName = realmFileName.replace(c, '_');
}

// Create the folder on disk (if needed)
if (!realmFileDirectory.exists() && !realmFileDirectory.mkdirs()) {
throw new IllegalStateException("Could not create directory for saving the Realm: " + realmFileDirectory);
}
String realmFileName = overrideDefaultLocalFileName ? fileName : defaultLocalFileName;

return new SyncConfiguration(
// Realm Configuration options
Expand Down

0 comments on commit 7526eaa

Please sign in to comment.