// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using EpicGames.Core; using EpicGames.Serialization; namespace EpicGames.Perforce.Managed { /// /// Metadata for a Perforce file /// [DebuggerDisplay("{Path}")] public class StreamFile { /// /// Depot path for this file /// public Utf8String Path { get; } /// /// Length of the file, as reported by the server (actual size on disk may be different due to workspace options). /// public long Length { get; } /// /// Unique identifier for the file content /// public FileContentId ContentId { get; } /// /// Revision number of the file /// public int Revision { get; } #region Field names static readonly Utf8String s_lengthField = new Utf8String("len"); static readonly Utf8String s_digestField = new Utf8String("dig"); static readonly Utf8String s_typeField = new Utf8String("type"); static readonly Utf8String s_revisionField = new Utf8String("rev"); #endregion /// /// Constructor /// public StreamFile(string path, long length, FileContentId contentId, int revision) : this(new Utf8String(path), length, contentId, revision) { } /// /// Constructor /// public StreamFile(Utf8String path, long length, FileContentId contentId, int revision) { Path = path; Length = length; ContentId = contentId; Revision = revision; } /// /// Parse from a compact binary object /// /// Path to the file /// /// public StreamFile(Utf8String path, CbObject field) { Path = path; Length = field[s_lengthField].AsInt64(); Md5Hash digest = new Md5Hash(field[s_digestField].AsBinary()); Utf8String type = field[s_typeField].AsUtf8String(); ContentId = new FileContentId(digest, type); Revision = field[s_revisionField].AsInt32(); } /// /// Write this object to compact binary /// /// public void Write(CbWriter writer) { writer.WriteInteger(s_lengthField, Length); writer.WriteBinarySpan(s_digestField, ContentId.Digest.Span); writer.WriteUtf8String(s_typeField, ContentId.Type); writer.WriteInteger(s_revisionField, Revision); } /// public override string ToString() { return $"StreamFile({Path}#{Revision} Len={Length} Digest={ContentId.Digest} Type={ContentId.Type})"; } } /// /// Stores a reference to another tree /// public class StreamTreeRef { /// /// Base depot path for the directory /// public Utf8String Path { get; set; } /// /// Hash of the tree /// public IoHash Hash { get; set; } #region Field names static readonly Utf8String s_hashField = new Utf8String("hash"); #endregion /// /// Constructor /// /// /// public StreamTreeRef(Utf8String path, IoHash hash) { Path = path; Hash = hash; } /// /// Constructor /// /// /// public StreamTreeRef(Utf8String path, CbObject field) { Path = path; Hash = field[s_hashField].AsObjectAttachment(); } /// /// Gets the hash of this reference /// /// public IoHash ComputeHash() { CbWriter writer = new CbWriter(); writer.BeginObject(); Write(writer); writer.EndObject(); return writer.ToObject().GetHash(); } /// /// Serialize to a compact binary object /// /// public void Write(CbWriter writer) { writer.WriteObjectAttachment(s_hashField, Hash); } } /// /// Information about a directory within a stream /// public class StreamTree { /// /// The path to this tree /// public Utf8String Path { get; } /// /// Map of name to file within the directory /// public Dictionary NameToFile { get; } = new Dictionary(); /// /// Map of name to subdirectory /// public Dictionary NameToTree { get; } = new Dictionary(FileUtils.PlatformPathComparerUtf8); #region Field names static readonly Utf8String s_nameField = new Utf8String("name"); static readonly Utf8String s_pathField = new Utf8String("path"); static readonly Utf8String s_filesField = new Utf8String("files"); static readonly Utf8String s_treesField = new Utf8String("trees"); #endregion /// /// Default constructor /// public StreamTree() { } /// /// Default constructor /// public StreamTree(Utf8String path, Dictionary nameToFile, Dictionary nameToTree) { CheckPath(path); Path = path; NameToFile = nameToFile; NameToTree = nameToTree; } /// /// Deserialize a tree from a compact binary object /// public StreamTree(Utf8String path, CbObject @object) { CheckPath(path); Path = path; CbArray fileArray = @object[s_filesField].AsArray(); foreach (CbField fileField in fileArray) { CbObject fileObject = fileField.AsObject(); Utf8String name = fileObject[s_nameField].AsUtf8String(); Utf8String filePath = ReadPath(fileObject, path, name); StreamFile file = new StreamFile(filePath, fileObject); NameToFile.Add(name, file); } CbArray treeArray = @object[s_treesField].AsArray(); foreach (CbField treeField in treeArray) { CbObject treeObject = treeField.AsObject(); Utf8String name = treeObject[s_nameField].AsUtf8String(); Utf8String treePath = ReadPath(treeObject, path, name); StreamTreeRef tree = new StreamTreeRef(treePath, treeObject); NameToTree.Add(name, tree); } } /// /// Serialize to a compact binary object /// /// public void Write(CbWriter writer) { if (NameToFile.Count > 0) { writer.BeginArray(s_filesField); foreach ((Utf8String name, StreamFile file) in NameToFile.OrderBy(x => x.Key)) { writer.BeginObject(); writer.WriteUtf8String(s_nameField, name); WritePath(writer, file.Path, Path, name); file.Write(writer); writer.EndObject(); } writer.EndArray(); } if (NameToTree.Count > 0) { writer.BeginArray(s_treesField); foreach ((Utf8String name, StreamTreeRef tree) in NameToTree.OrderBy(x => x.Key)) { writer.BeginObject(); writer.WriteUtf8String(s_nameField, name); WritePath(writer, tree.Path, Path, name); tree.Write(writer); writer.EndObject(); } writer.EndArray(); } } /// /// Reads a path from an object, defaulting it to the parent path plus the child name /// /// /// /// /// static Utf8String ReadPath(CbObject @object, Utf8String basePath, Utf8String name) { Utf8String path = @object[s_pathField].AsUtf8String(); if (path.IsEmpty) { byte[] data = new byte[basePath.Length + 1 + name.Length]; basePath.Memory.CopyTo(data); data[basePath.Length] = (byte)'/'; name.Memory.CopyTo(data.AsMemory(basePath.Length + 1)); path = new Utf8String(data); } return path; } /// /// Writes a path if it's not the default (the parent path, a slash, followed by the child name) /// /// /// /// /// static void WritePath(CbWriter writer, Utf8String path, Utf8String parentPath, Utf8String name) { if (path.Length != parentPath.Length + name.Length + 1 || !path.StartsWith(parentPath) || path[parentPath.Length] != '/' || !path.EndsWith(name)) { writer.WriteUtf8String(s_pathField, path); } } /// /// Checks that a base path does not have a trailing slash /// /// /// static void CheckPath(Utf8String path) { if (path.Length > 0 && path[^1] == '/') { throw new ArgumentException("BasePath must not end in a slash", nameof(path)); } } /// /// Convert to a compact binary object /// /// public CbObject ToCbObject() { CbWriter writer = new CbWriter(); writer.BeginObject(); Write(writer); writer.EndObject(); return writer.ToObject(); } } }