< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 24
Coverable lines: 24
Total lines: 233
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 14
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: ValidIdPattern()100%210%
File 2: .cctor()100%210%
File 2: IsValid(...)0%7280%
File 2: ParseId(...)0%4260%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/chronicis/chronicis/src/Chronicis.Api/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ExternalLinks/BlobIdValidator.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2
 3namespace Chronicis.Api.Services.ExternalLinks;
 4
 5/// <summary>
 6/// Validates and parses blob-based external link IDs.
 7/// Security: Prevents path traversal and injection attacks.
 8/// Supports hierarchical IDs like "items/armor/breastplate" for subcategories.
 9/// </summary>
 10public static partial class BlobIdValidator
 11{
 12    // ID must be: alphanumeric + hyphens (case-insensitive), one or more slashes
 13    // Format: "category/slug" OR "category/subcategory/slug"
 14    // Examples: "spells/fireball", "items/armor/breastplate", "bestiary/Beast/aboar"
 15    [GeneratedRegex(@"^[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+)+$", RegexOptions.Compiled)]
 16    private static partial Regex ValidIdPattern();
 17
 18    // Characters that are prohibited in IDs (security)
 019    private static readonly char[] ProhibitedChars = { '.', '\\', '[', ']', '|' };
 20
 21    /// <summary>
 22    /// Validates an external link ID against security rules.
 23    /// Supports hierarchical paths with multiple slashes (e.g., "items/armor/breastplate").
 24    /// </summary>
 25    /// <param name="id">ID to validate.</param>
 26    /// <param name="error">Error message if validation fails.</param>
 27    /// <returns>True if valid, false otherwise.</returns>
 28    public static bool IsValid(string id, out string? error)
 29    {
 030        error = null;
 31
 032        if (string.IsNullOrWhiteSpace(id))
 33        {
 034            error = "ID cannot be empty";
 035            return false;
 36        }
 37
 38        // Check for prohibited characters (path traversal, injection attempts)
 039        if (id.IndexOfAny(ProhibitedChars) >= 0)
 40        {
 041            error = "ID contains prohibited characters (. \\ [ ] |)";
 042            return false;
 43        }
 44
 45        // Check for path traversal patterns
 046        if (id.Contains("..", StringComparison.Ordinal))
 47        {
 048            error = "ID contains path traversal pattern (..)";
 049            return false;
 50        }
 51
 52        // Validate format: lowercase alphanumeric + hyphens with at least one slash
 053        if (!ValidIdPattern().IsMatch(id))
 54        {
 055            error = "ID must have format 'category/slug' or 'category/subcategory/slug' with alphanumeric and hyphens on
 056            return false;
 57        }
 58
 059        return true;
 60    }
 61
 62    /// <summary>
 63    /// Parses a validated ID into category path and slug components.
 64    /// For hierarchical IDs, category includes all path segments except the last.
 65    /// </summary>
 66    /// <param name="id">ID to parse (e.g., "spells/fireball" or "items/armor/breastplate").</param>
 67    /// <returns>
 68    /// Tuple of (categoryPath, slug):
 69    /// - "spells/fireball" -> ("spells", "fireball")
 70    /// - "items/armor/breastplate" -> ("items/armor", "breastplate")
 71    /// Returns (null, null) if invalid format.
 72    /// </returns>
 73    public static (string? categoryPath, string? slug) ParseId(string id)
 74    {
 075        if (string.IsNullOrWhiteSpace(id))
 76        {
 077            return (null, null);
 78        }
 79
 080        var lastSlashIndex = id.LastIndexOf('/');
 081        if (lastSlashIndex <= 0 || lastSlashIndex >= id.Length - 1)
 82        {
 83            // No slash, or slash at start/end
 084            return (null, null);
 85        }
 86
 087        var categoryPath = id[..lastSlashIndex];
 088        var slug = id[(lastSlashIndex + 1)..];
 89
 090        return (categoryPath, slug);
 91    }
 92}