Fixes some issues with username storage and handling

- Fix the yaml/json UUID cache corrupting username data when performing uuid --> username checks
- Fix handling of usernames when users are loaded

Closes #301
This commit is contained in:
Luck 2017-06-09 18:20:12 +01:00
parent ff0c988a1d
commit 5c5d1e8d0a
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
11 changed files with 221 additions and 89 deletions

View File

@ -40,6 +40,7 @@ import me.lucko.luckperms.api.context.ContextSet;
import me.lucko.luckperms.api.context.ImmutableContextSet;
import me.lucko.luckperms.api.context.MutableContextSet;
import me.lucko.luckperms.common.core.NodeFactory;
import me.lucko.luckperms.common.utils.DateUtil;
import me.lucko.luckperms.common.utils.PatternCache;
import me.lucko.luckperms.common.utils.ShorthandParser;
@ -314,12 +315,12 @@ public final class ImmutableNode implements Node {
@Override
public long getSecondsTilExpiry() {
Preconditions.checkState(isTemporary(), "Node does not have an expiry time.");
return expireAt - (System.currentTimeMillis() / 1000L);
return expireAt - DateUtil.unixSecondsNow();
}
@Override
public boolean hasExpired() {
return isTemporary() && expireAt < (System.currentTimeMillis() / 1000L);
return isTemporary() && expireAt < DateUtil.unixSecondsNow();
}
@Override

View File

@ -95,7 +95,7 @@ public class User extends PermissionHolder implements Identifiable<UserIdentifie
super(uuid.toString(), plugin);
this.uuid = uuid;
setName(name, true);
setName(name, false);
this.primaryGroup = plugin.getConfiguration().get(ConfigKeys.PRIMARY_GROUP_CALCULATION).apply(this);
this.userData = new UserCache(this);
getPlugin().getApiProvider().getEventFactory().handleUserCacheLoad(this, userData);
@ -110,41 +110,51 @@ public class User extends PermissionHolder implements Identifiable<UserIdentifie
return Optional.ofNullable(name);
}
public boolean setName(String name, boolean force) {
// if the value being set is null
if (name == null || name.equalsIgnoreCase("null") || name.isEmpty()) {
// only apply the change if it is being forced
if (force) {
// if the name is already null, return false
if (this.name == null) {
return false;
} else {
// set the new null value
this.name = null;
return true;
/**
* Sets the users name
*
* @param name the name to set
* @param weak if true, the value will only be updated if a value hasn't been set previously.
* @return true if a change was made
*/
public boolean setName(String name, boolean weak) {
// if weak is true, only update the value in the User if it's null
if (weak && this.name != null) {
// try to update casing if they're equalIgnoreCase
if (name != null && this.name.equalsIgnoreCase(name)) {
this.name = name;
}
} else {
// we already have a non-null value, so return false
return false;
}
} else {
// the name being set is not null
if (this.name == null) {
// consistency. if the name being set is equivalent to null, just make it null.
if (name != null && (name.isEmpty() || name.equalsIgnoreCase("null"))) {
name = null;
}
// if one or the other is null, just update and return true
if ((this.name == null) != (name == null)) {
this.name = name;
return true;
}
// update the capitalisation, but still return false
if (this.name == null) {
// they're both null
return false;
} else {
// both non-null
if (this.name.equalsIgnoreCase(name)) {
this.name = name;
this.name = name; // update case anyway, but return false
return false;
}
// completely new value, just set & return true
} else {
this.name = name;
return true;
}
}
}
@Override
public String getFriendlyName() {

View File

@ -40,6 +40,7 @@ import me.lucko.luckperms.common.constants.Constants;
import me.lucko.luckperms.common.constants.Message;
import me.lucko.luckperms.common.constants.Permission;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.DateUtil;
import io.github.mkremins.fanciful.FancyMessage;
@ -96,7 +97,7 @@ public class Importer implements Runnable {
int index = 1;
for (String command : commands) {
long time = System.currentTimeMillis() / 1000;
long time = DateUtil.unixSecondsNow();
if (lastMsg < (time - PROGRESS_REPORT_SECONDS)) {
lastMsg = time;

View File

@ -34,6 +34,7 @@ import me.lucko.luckperms.common.core.model.PermissionHolder;
import me.lucko.luckperms.common.core.model.Track;
import me.lucko.luckperms.common.core.model.User;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.DateUtil;
import java.util.List;
@ -135,7 +136,7 @@ public class LogEntry extends me.lucko.luckperms.api.LogEntry {
@Override
public LogEntry build() {
if (getTimestamp() == 0L) {
super.timestamp(System.currentTimeMillis() / 1000L);
super.timestamp(DateUtil.unixSecondsNow());
}
return super.build();

View File

@ -37,25 +37,18 @@ import me.lucko.luckperms.common.data.Log;
import me.lucko.luckperms.common.managers.GroupManager;
import me.lucko.luckperms.common.managers.TrackManager;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.backing.utils.FileUuidCache;
import me.lucko.luckperms.common.storage.backing.utils.LegacyJSONSchemaMigration;
import me.lucko.luckperms.common.storage.backing.utils.LegacyYAMLSchemaMigration;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
@ -67,7 +60,7 @@ public abstract class FlatfileBacking extends AbstractBacking {
private static final String LOG_FORMAT = "%s(%s): [%s] %s(%s) --> %s";
private final Logger actionLogger = Logger.getLogger("luckperms_actions");
private Map<String, String> uuidCache = new ConcurrentHashMap<>();
private FileUuidCache uuidCache = new FileUuidCache();
private final File pluginDir;
@ -98,7 +91,7 @@ public abstract class FlatfileBacking extends AbstractBacking {
return;
}
uuidCache.putAll(getUUIDCache());
uuidCache.load(uuidData);
try {
FileHandler fh = new FileHandler(actionLog.getAbsolutePath(), 0, 1, true);
@ -207,7 +200,7 @@ public abstract class FlatfileBacking extends AbstractBacking {
@Override
public void shutdown() {
saveUUIDCache(uuidCache);
uuidCache.save(uuidData);
}
protected void registerFileAction(String type, File file) {
@ -325,51 +318,17 @@ public abstract class FlatfileBacking extends AbstractBacking {
@Override
public boolean saveUUIDData(String username, UUID uuid) {
username = username.toLowerCase();
uuidCache.put(username, uuid.toString());
uuidCache.addMapping(username, uuid);
return true;
}
@Override
public UUID getUUID(String username) {
username = username.toLowerCase();
if (uuidCache.get(username) == null) return null;
return UUID.fromString(uuidCache.get(username));
return uuidCache.lookupUUID(username);
}
@Override
public String getName(UUID uuid) {
for (Map.Entry<String, String> e : uuidCache.entrySet()) {
if (e.getValue().equalsIgnoreCase(uuid.toString())) {
return e.getKey();
}
}
return null;
}
private Map<String, String> getUUIDCache() {
Map<String, String> cache = new HashMap<>();
try (BufferedReader reader = Files.newBufferedReader(uuidData.toPath(), StandardCharsets.UTF_8)) {
Properties props = new Properties();
props.load(reader);
for (String key : props.stringPropertyNames()) {
cache.put(key, props.getProperty(key));
}
} catch (IOException e) {
e.printStackTrace();
}
return cache;
}
private void saveUUIDCache(Map<String, String> cache) {
try (BufferedWriter writer = Files.newBufferedWriter(uuidData.toPath(), StandardCharsets.UTF_8)) {
Properties properties = new Properties();
properties.putAll(cache);
properties.store(writer, null);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
return uuidCache.lookupUsername(uuid);
}
}

View File

@ -171,10 +171,10 @@ public class JSONBacking extends FlatfileBacking {
Set<NodeModel> data = deserializePermissions(object.get("permissions").getAsJsonArray());
Set<Node> nodes = data.stream().map(NodeModel::toNode).collect(Collectors.toSet());
user.setNodes(nodes);
user.setName(name, true);
boolean save = plugin.getUserManager().giveDefaultIfNeeded(user, false);
if (user.setName(name, false)) {
if (user.getName().isPresent() && (name == null || !user.getName().get().equalsIgnoreCase(name))) {
save = true;
}

View File

@ -328,10 +328,10 @@ public class MongoDBBacking extends AbstractBacking {
.collect(Collectors.toSet())
);
user.getPrimaryGroup().setStoredValue(d.getString("primaryGroup"));
user.setName(name, true);
boolean save = plugin.getUserManager().giveDefaultIfNeeded(user, false);
if (user.setName(d.getString("name"), false)) {
if (user.getName().isPresent() && (name == null || !user.getName().get().equalsIgnoreCase(name))) {
save = true;
}

View File

@ -348,7 +348,7 @@ public class SQLBacking extends AbstractBacking {
user.getPrimaryGroup().setStoredValue(pg);
// Update their username to what was in the storage if the one in the local instance is null
user.setName(userName.get(), false);
user.setName(userName.get(), true);
// If the user has any data in storage
if (!data.isEmpty()) {

View File

@ -175,10 +175,10 @@ public class YAMLBacking extends FlatfileBacking {
Set<NodeModel> data = deserializePermissions((List<Object>) values.get("permissions"));
Set<Node> nodes = data.stream().map(NodeModel::toNode).collect(Collectors.toSet());
user.setNodes(nodes);
user.setName(name, true);
boolean save = plugin.getUserManager().giveDefaultIfNeeded(user, false);
if (user.setName(name, false)) {
if (user.getName().isPresent() && (name == null || !user.getName().get().equalsIgnoreCase(name))) {
save = true;
}

View File

@ -0,0 +1,156 @@
package me.lucko.luckperms.common.storage.backing.utils;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import me.lucko.luckperms.common.utils.DateUtil;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class FileUuidCache {
private static final Splitter KV_SPLIT = Splitter.on('=').omitEmptyStrings();
private static final Splitter TIME_SPLIT = Splitter.on('|').omitEmptyStrings();
// the map for lookups
private final Map<String, Map.Entry<UUID, Long>> lookupMap = new ConcurrentHashMap<>();
/**
* Adds a mapping to the cache
*
* @param username the username of the player
* @param uuid the uuid of the player
*/
public void addMapping(String username, UUID uuid) {
lookupMap.put(username.toLowerCase(), Maps.immutableEntry(uuid, DateUtil.unixSecondsNow()));
}
/**
* Gets the most recent uuid which connected with the given username, or null
*
* @param username the username to lookup with
* @return a uuid, or null
*/
public UUID lookupUUID(String username) {
Map.Entry<UUID, Long> ret = lookupMap.get(username.toLowerCase());
return ret == null ? null : ret.getKey();
}
/**
* Gets the most recent username used by a given uuid
*
* @param uuid the uuid to lookup with
* @return a username, or null
*/
public String lookupUsername(UUID uuid) {
String username = null;
Long time = Long.MIN_VALUE;
for (Map.Entry<String, Map.Entry<UUID, Long>> ent : lookupMap.entrySet()) {
if (!ent.getValue().getKey().equals(uuid)) {
continue;
}
Long t = ent.getValue().getValue();
if (t > time) {
time = t;
username = ent.getKey();
}
}
return username;
}
public void load(File file) {
if (!file.exists()) {
return;
}
try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
String entry;
while ((entry = reader.readLine()) != null) {
entry = entry.trim();
if (entry.isEmpty() || entry.startsWith("#")) {
continue;
}
Iterator<String> parts = KV_SPLIT.split(entry).iterator();
if (!parts.hasNext()) continue;
String key = parts.next();
if (!parts.hasNext()) continue;
String value = parts.next();
UUID uid;
Long t;
// contains a time (backwards compat)
if (value.contains("|")) {
// try to split and extract the time element from the end.
Iterator<String> valueParts = TIME_SPLIT.split(value).iterator();
if (!valueParts.hasNext()) continue;
String uuid = valueParts.next();
if (!valueParts.hasNext()) continue;
String time = valueParts.next();
try {
uid = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
continue;
}
try {
t = Long.parseLong(time);
} catch (NumberFormatException e) {
continue;
}
} else {
// just parse from the value
try {
uid = UUID.fromString(value);
} catch (IllegalArgumentException e) {
continue;
}
t = 0L;
}
lookupMap.put(key, Maps.immutableEntry(uid, t));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void save(File file) {
try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
writer.write("# LuckPerms UUID lookup cache");
writer.newLine();
for (Map.Entry<String, Map.Entry<UUID, Long>> ent : lookupMap.entrySet()) {
String out = ent.getKey() + "=" + ent.getValue().getKey().toString() + "|" + ent.getValue().getValue().toString();
writer.write(out);
writer.newLine();
}
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -46,8 +46,12 @@ public class DateUtil {
private static final Pattern TIME_PATTERN = Pattern.compile("(?:([0-9]+)\\s*y[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*mo[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*w[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*d[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*h[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*m[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*(?:s[a-z]*)?)?", Pattern.CASE_INSENSITIVE);
private static final int MAX_YEARS = 100000;
public static long unixSecondsNow() {
return System.currentTimeMillis() / 1000L;
}
public static boolean shouldExpire(long unixTime) {
return unixTime < (System.currentTimeMillis() / 1000);
return unixTime < (unixSecondsNow());
}
/**