| File: Donor\ProjectDataDonorIndex.Serialization.cs | Web Access |
| Project: Microsoft.NET.ProjectData.csproj (Microsoft.NET.ProjectData) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Diagnostics; using System.Text.Json; namespace Microsoft.NET.ProjectData; public static partial class ProjectDataDonorIndex { private static readonly int ProcessId = GetCurrentProcessId(); private static ProjectDataDonorIndexFile ReadIndex(string indexPath) { if (!File.Exists(indexPath)) { return new ProjectDataDonorIndexFile { Version = CurrentVersion }; } using FileStream stream = new(indexPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using JsonDocument document = JsonDocument.Parse(stream); JsonElement root = document.RootElement; if (root.ValueKind != JsonValueKind.Object) { throw new JsonException($"Donor index '{indexPath}' must contain a JSON object."); } int version = GetInt32(root, "version") ?? 0; ProjectDataDonorIndexFile index = new() { Version = version }; if (version != CurrentVersion) { return index; } if (root.TryGetProperty("entries", out JsonElement entries)) { RequireValueKind(entries, JsonValueKind.Array, "entries"); foreach (JsonElement item in entries.EnumerateArray()) { index.Entries.Add(ReadEntry(item)); } } return index; } private static ProjectDataDonorIndexFile ReadIndexForWrite(string indexPath, out string? recoveryMessage) { recoveryMessage = null; try { ProjectDataDonorIndexFile index = ReadIndex(indexPath); if (index.Version == CurrentVersion) { return index; } if (index.Version > CurrentVersion) { throw new InvalidOperationException( $"Cannot update unsupported donor index version {index.Version}; expected {CurrentVersion}."); } } catch (JsonException) { } string quarantinePath = indexPath + ".corrupt-" + DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff") + "-" + Guid.NewGuid().ToString("N"); File.Move(indexPath, quarantinePath); recoveryMessage = $"Recovered corrupt donor index '{indexPath}'; the original was preserved at '{quarantinePath}'."; return new ProjectDataDonorIndexFile { Version = CurrentVersion }; } private static ProjectDataDonorIndexEntry ReadEntry(JsonElement element) { RequireValueKind(element, JsonValueKind.Object, "index entry"); string? path = GetString(element, "path"); if (path is not { Length: > 0 } entryPath) { throw new JsonException("Donor index entries must contain a non-empty string path."); } string normalizedPath; try { normalizedPath = Path.GetFullPath(entryPath); } catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) { throw new JsonException("Donor index entries must contain a valid path.", ex); } return new ProjectDataDonorIndexEntry { Path = normalizedPath, NewestMtimeMs = GetInt64(element, "newestMtimeMs"), UpdatedUtc = GetDateTimeOffset(element, "updatedUtc"), }; } private static void WriteIndex(string indexPath, ProjectDataDonorIndexFile index) { Directory.CreateDirectory(Path.GetDirectoryName(indexPath)!); string tempPath = indexPath + "." + ProcessId + "." + Guid.NewGuid().ToString("N") + ".tmp"; try { using (FileStream stream = File.Create(tempPath)) using (Utf8JsonWriter writer = new(stream, new JsonWriterOptions { Indented = true })) { writer.WriteStartObject(); writer.WriteNumber("version", CurrentVersion); writer.WritePropertyName("entries"); writer.WriteStartArray(); foreach (ProjectDataDonorIndexEntry entry in index.Entries) { WriteEntry(writer, entry); } writer.WriteEndArray(); writer.WriteEndObject(); } ReplaceOrMove(tempPath, indexPath); } finally { try { File.Delete(tempPath); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { Trace.TraceWarning("Failed to delete temporary donor index file {0}: {1}", tempPath, ex.Message); } } } private static int GetCurrentProcessId() { using Process process = Process.GetCurrentProcess(); return process.Id; } private static void WriteEntry(Utf8JsonWriter writer, ProjectDataDonorIndexEntry entry) { writer.WriteStartObject(); writer.WriteString("path", entry.Path); WriteOptionalNumber(writer, "newestMtimeMs", entry.NewestMtimeMs); writer.WriteString("updatedUtc", (entry.UpdatedUtc ?? DateTimeOffset.UtcNow).UtcDateTime); writer.WriteEndObject(); } private static void WriteOptionalNumber(Utf8JsonWriter writer, string propertyName, long? value) { if (value.HasValue) { writer.WriteNumber(propertyName, value.Value); } } private static void ReplaceOrMove(string tempPath, string indexPath) { if (File.Exists(indexPath)) { File.Replace(tempPath, indexPath, destinationBackupFileName: null); return; } File.Move(tempPath, indexPath); } private static string? GetString(JsonElement element, string name) { if (!element.TryGetProperty(name, out JsonElement property) || property.ValueKind == JsonValueKind.Null) { return null; } RequireValueKind(property, JsonValueKind.String, name); return property.GetString(); } private static int? GetInt32(JsonElement element, string name) { if (!element.TryGetProperty(name, out JsonElement property)) { return null; } if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out int value)) { return value; } throw new JsonException($"Donor index property '{name}' must be a 32-bit integer."); } private static long? GetInt64(JsonElement element, string name) { if (!element.TryGetProperty(name, out JsonElement property)) { return null; } if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out long value)) { return value; } throw new JsonException($"Donor index property '{name}' must be a 64-bit integer."); } private static DateTimeOffset? GetDateTimeOffset(JsonElement element, string name) { if (!element.TryGetProperty(name, out JsonElement property)) { return null; } if (property.ValueKind == JsonValueKind.String && property.TryGetDateTimeOffset(out DateTimeOffset value)) { return value; } throw new JsonException($"Donor index property '{name}' must be a date-time string."); } private static void RequireValueKind(JsonElement element, JsonValueKind expectedKind, string propertyName) { if (element.ValueKind != expectedKind) { throw new JsonException($"Donor index property '{propertyName}' must be {expectedKind}."); } } private sealed class ProjectDataDonorIndexFile { public int Version { get; set; } public List<ProjectDataDonorIndexEntry> Entries { get; } = []; public void UpsertEntry(ProjectDataDonorIndexEntry entry) { ProjectDataDonorIndexEntry? existingEntry = this.Entries.FirstOrDefault(existing => PathsEqual(existing.Path, entry.Path)); if (existingEntry?.NewestMtimeMs > entry.NewestMtimeMs) { entry.NewestMtimeMs = existingEntry.NewestMtimeMs; } this.Entries.RemoveAll(existing => PathsEqual(existing.Path, entry.Path)); this.Entries.Insert(0, entry); } } }