// Copyright Epic Games, Inc. All Rights Reserved. using System; using EpicGames.Core; namespace EpicGames.Horde.Storage { /// /// Path to an object within an . Object keys are normalized to lowercase, and may consist of the characters [a-z0-9_./]. /// [JsonSchemaString] public readonly struct ObjectKey : IEquatable { /// /// Dummy enum to allow invoking the constructor which takes a sanitized full path /// public enum Validate { /// /// Dummy value /// None } readonly Utf8String _path; /// /// Accessor for the internal path string /// public Utf8String Path => _path; /// /// Constructor /// /// Path to the blob. The meaning of this string is implementation defined. public ObjectKey(string path) : this(new Utf8String(path)) { } /// /// Constructor /// /// Path to the blob. The meaning of this string is implementation defined. public ObjectKey(Utf8String path) { _path = path; if (path.Length > 0) { if (path[0] == '/' || path[^1] == '/') { throw new FormatException($"Object locator '{path}' is invalid; locators may not start or end with a slash"); } for (int idx = 0; idx < path.Length; idx++) { byte character = path[idx]; if (!StringId.IsValidCharacter(character)) { if (character >= 'A' && character <= 'Z') { // Allow uppercase characters for now } else if (character == '+') { // Allow plus characters for now } else if (path[idx] == '/' && path[idx - 1] != '/') { // Non-consecutive path separator; allowed. } else { throw new FormatException($"Object locator '{path}' is invalid; character '{(char)path[idx]}' is not allowed"); } } } } } /// /// Constructor /// /// Path to the blob. The meaning of this string is implementation defined. /// public ObjectKey(Utf8String path, Validate validate) { _path = path; _ = validate; } /// /// Makes an object key from the given path /// public static ObjectKey Sanitize(Utf8String path) { byte[]? data = null; for (int idx = 0; idx < path.Length; idx++) { byte character = path[idx]; if (!StringId.IsValidCharacter(character) && character != '/') { data ??= path.Memory.ToArray(); if (character >= 'A' && character <= 'Z') { data[idx] = (byte)('a' + (character - 'A')); } else { data[idx] = (byte)'_'; } } } return new ObjectKey((data == null) ? path : new Utf8String(data)); } /// /// Whether the blob locator is valid /// public bool IsValid() => !_path.IsEmpty; /// public override bool Equals(object? obj) => obj is BlobLocator blobId && Equals(blobId); /// public override int GetHashCode() => _path.GetHashCode(); /// public bool Equals(ObjectKey other) => _path == other._path; /// public override string ToString() => _path.ToString(); /// public static bool operator ==(ObjectKey left, ObjectKey right) => left.Path == right.Path; /// public static bool operator !=(ObjectKey left, ObjectKey right) => !(left == right); } }