/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.database.utilities;
import static com.google.firebase.database.utilities.Utilities.hardAssert;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.core.ServerValues;
import com.google.firebase.database.core.ValidationPath;
import com.google.firebase.database.snapshot.ChildKey;
import com.google.firebase.database.snapshot.Node;
import com.google.firebase.database.snapshot.NodeUtilities;
import com.google.firebase.database.snapshot.PriorityUtilities;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
public class Validation {
private static final Pattern INVALID_PATH_REGEX = Pattern.compile("[\\[\\]\\.#$]");
// CSOFF: AvoidEscapedUnicodeCharacters
private static final Pattern INVALID_KEY_REGEX =
Pattern.compile("[\\[\\]\\.#\\$\\/\\u0000-\\u001F\\u007F]");
// CSON: AvoidEscapedUnicodeCharacters
private static boolean isValidPathString(String pathString) {
return !INVALID_PATH_REGEX.matcher(pathString).find();
}
public static void validatePathString(String pathString) throws DatabaseException {
if (!isValidPathString(pathString)) {
throw new DatabaseException(
"Invalid Firebase Database path: "
+ pathString
+ ". Firebase Database paths must not contain '.', '#', '$', '[', or ']'");
}
}
public static void validateRootPathString(String pathString) throws DatabaseException {
if (pathString.startsWith(".info")) {
validatePathString(pathString.substring(5));
} else if (pathString.startsWith("/.info")) {
validatePathString(pathString.substring(6));
} else {
validatePathString(pathString);
}
}
private static boolean isWritableKey(String key) {
return key != null
&& key.length() > 0
&& (key.equals(".value")
|| key.equals(".priority")
|| (!key.startsWith(".") && !INVALID_KEY_REGEX.matcher(key).find()));
}
private static boolean isValidKey(String key) {
return key.equals(".info") || !INVALID_KEY_REGEX.matcher(key).find();
}
public static void validateNullableKey(String key) throws DatabaseException {
if (!(key == null || isValidKey(key))) {
throw new DatabaseException(
"Invalid key: " + key + ". Keys must not contain '/', '.', '#', '$', '[', or ']'");
}
}
private static boolean isWritablePath(Path path) {
// Getting a path with invalid keys will throw earlier in the process, so we should just
// check the first token
ChildKey front = path.getFront();
return front == null || !front.asString().startsWith(".");
}
@SuppressWarnings("unchecked")
public static void validateWritableObject(Object object) {
if (object instanceof Map) {
Map<String, Object> map = (Map<String, Object>) object;
if (map.containsKey(ServerValues.NAME_SUBKEY_SERVERVALUE)) {
// This will be short-circuited by conversion and we consider it valid
return;
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
validateWritableKey(entry.getKey());
validateWritableObject(entry.getValue());
}
} else if (object instanceof List) {
List<Object> list = (List<Object>) object;
for (Object child : list) {
validateWritableObject(child);
}
} else {
// It's a primitive, should be fine
}
}
public static void validateWritableKey(String key) throws DatabaseException {
if (!isWritableKey(key)) {
throw new DatabaseException(
"Invalid key: " + key + ". Keys must not contain '/', '.', '#', '$', '[', or ']'");
}
}
public static void validateWritablePath(Path path) throws DatabaseException {
if (!isWritablePath(path)) {
throw new DatabaseException("Invalid write location: " + path.toString());
}
}
public static Map<Path, Node> parseAndValidateUpdate(Path path, Map<String, Object> update)
throws DatabaseException {
final SortedMap<Path, Node> parsedUpdate = new TreeMap<>();
for (Map.Entry<String, Object> entry : update.entrySet()) {
Path updatePath = new Path(entry.getKey());
Object newValue = entry.getValue();
ValidationPath.validateWithObject(path.child(updatePath), newValue);
String childName = !updatePath.isEmpty() ? updatePath.getBack().asString() : "";
if (childName.equals(ServerValues.NAME_SUBKEY_SERVERVALUE)
|| childName.equals("" + ".value")) {
throw new DatabaseException(
"Path '" + updatePath + "' contains disallowed child name: " + childName);
}
if (childName.equals(".priority")) {
if (!PriorityUtilities.isValidPriority(NodeUtilities.NodeFromJSON(newValue))) {
throw new DatabaseException(
"Path '"
+ updatePath
+ "' contains invalid priority "
+ "(must be a string, double, ServerValue, or null).");
}
}
Validation.validateWritableObject(newValue);
parsedUpdate.put(updatePath, NodeUtilities.NodeFromJSON(newValue));
}
// Check that update keys are not ancestors of each other.
Path prevPath = null;
for (Path curPath : parsedUpdate.keySet()) {
// We rely on the property that sorting guarantees that ancestors come right before
// descendants.
hardAssert(prevPath == null || prevPath.compareTo(curPath) < 0);
if (prevPath != null && prevPath.contains(curPath)) {
throw new DatabaseException(
"Path '" + prevPath + "' is an ancestor of '" + curPath + "' in an update.");
}
prevPath = curPath;
}
return parsedUpdate;
}
}