using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using OpenNest; using OpenNest.Data; namespace OpenNest.Server; /// /// Parses drawing library requests. Unknown, repeated or out-of-range values are /// rejected rather than ignored or clamped, and enum fields accept names only. /// internal static class DrawingLibraryRequest { private static readonly string[] QueryKeys = { "search", "customer", "materialName", "materialGrade", "thickness", "cutType", "cutFirst", "includeArchived", "sort", "order", "offset", "limit", }; // Integer enum values are refused; names are additionally checked before deserializing. private static readonly JsonSerializerOptions MetadataOptions = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false) }, }; public static bool TryParseQuery(IQueryCollection values, out DrawingLibraryQuery query, out string error) { query = new DrawingLibraryQuery(); foreach (var key in values.Keys) { if (!QueryKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) { error = $"Unknown query parameter '{key}'."; return false; } if (values[key].Count > 1) { error = $"Query parameter '{key}' was given more than once."; return false; } } var sort = DrawingSortField.SavedAt; if (values.TryGetValue("sort", out var sortText) && !TryEnumName(sortText.ToString(), out sort)) { error = $"sort must be one of: {string.Join(", ", Enum.GetNames().Select(CamelCase))}."; return false; } var descending = true; if (values.TryGetValue("order", out var orderText)) { if (string.Equals(orderText, "asc", StringComparison.OrdinalIgnoreCase)) { descending = false; } else if (!string.Equals(orderText, "desc", StringComparison.OrdinalIgnoreCase)) { error = "order must be asc or desc."; return false; } } CutType? cutType = null; if (values.TryGetValue("cutType", out var cutTypeText)) { if (!TryEnumName(cutTypeText.ToString(), out var parsed)) { error = "cutType must be Laser, Waterjet or Plasma."; return false; } cutType = parsed; } double? thickness = null; if (values.TryGetValue("thickness", out var thicknessText)) { if (!double.TryParse(thicknessText.ToString(), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var parsed) || !double.IsFinite(parsed)) { error = "thickness must be a finite number, zero or greater."; return false; } thickness = parsed; } var includeArchived = false; if (values.TryGetValue("includeArchived", out var archivedText) && !bool.TryParse(archivedText.ToString(), out includeArchived)) { error = "includeArchived must be true or false."; return false; } bool? cutFirst = null; if (values.TryGetValue("cutFirst", out var cutFirstText)) { if (!bool.TryParse(cutFirstText.ToString(), out var parsed)) { error = "cutFirst must be true or false."; return false; } cutFirst = parsed; } var offset = 0; var limit = DrawingLibraryQuery.DefaultLimit; if (!TryInteger(values, "offset", ref offset, out error) || !TryInteger(values, "limit", ref limit, out error)) return false; query = new DrawingLibraryQuery { Search = values["search"].ToString(), Customer = values["customer"].ToString(), MaterialName = values["materialName"].ToString(), MaterialGrade = values["materialGrade"].ToString(), Thickness = thickness, CutType = cutType, CutFirst = cutFirst, IncludeArchived = includeArchived, Sort = sort, Descending = descending, Offset = offset, Limit = limit, }; error = query.GetValidationError() ?? ""; return error.Length == 0; } /// Reads and validates one metadata JSON object. public static bool TryReadMetadata(string? json, out DrawingLibraryRecord record, out string error) { record = new DrawingLibraryRecord(); if (string.IsNullOrWhiteSpace(json)) { error = "Missing metadata."; return false; } try { using var document = JsonDocument.Parse(json); if (document.RootElement.ValueKind != JsonValueKind.Object) { error = "Metadata must be a JSON object."; return false; } // Enum converters also accept comma-separated combinations; allow single names only. foreach (var property in document.RootElement.EnumerateObject()) { if (string.Equals(property.Name, "cutType", StringComparison.OrdinalIgnoreCase)) { if (property.Value.ValueKind != JsonValueKind.String || !TryEnumName(property.Value.GetString()!, out _)) { error = "cutType must be Laser, Waterjet or Plasma."; return false; } } else if (string.Equals(property.Name, "units", StringComparison.OrdinalIgnoreCase)) { if (property.Value.ValueKind != JsonValueKind.String || !TryEnumName(property.Value.GetString()!, out _)) { error = "units must be a supported unit name."; return false; } } } record = document.RootElement.Deserialize(MetadataOptions) ?? record; } catch (JsonException) { error = "Invalid metadata JSON."; return false; } error = DrawingLibraryRules.GetMetadataError(record) ?? ""; return error.Length == 0; } /// /// Reads history-style paging: only offset (zero or greater) and limit /// (1 to ), each at most once. /// public static bool TryReadPaging(IQueryCollection values, out int offset, out int limit, out string error) { offset = 0; limit = DrawingLibraryRules.DefaultRevisionLimit; error = ""; foreach (var parameter in values) { if (parameter.Value.Count != 1 || !int.TryParse(parameter.Value[0], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var value)) { error = "Invalid history pagination."; return false; } switch (parameter.Key) { case "offset": offset = value; break; case "limit": limit = value; break; default: error = "Unknown history parameter."; return false; } } if (offset < 0 || limit < 1 || limit > DrawingLibraryRules.MaxRevisionLimit) { error = "Invalid history pagination."; return false; } return true; } // Names only: Enum.TryParse would also accept numbers, combinations and undefined values. internal static bool TryEnumName(string text, out TEnum value) where TEnum : struct, Enum { value = default; return text.Length > 0 && text.All(char.IsAsciiLetter) && Enum.TryParse(text, ignoreCase: true, out value) && Enum.IsDefined(value); } internal static string CamelCase(string name) => char.ToLowerInvariant(name[0]) + name[1..]; internal static bool TryInteger(IQueryCollection values, string key, ref int value, out string error) { error = ""; if (!values.TryGetValue(key, out var text)) return true; if (int.TryParse(text.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out value)) return true; error = $"{key} must be a non-negative integer."; return false; } }