Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/backend/Auth/PublisherNamePolicy.cs
blob: 4a4dcfb8ed4fa884b5f204251e194aff9bae8e45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0

namespace XSharp.Web.Api.Auth;

internal static class PublisherNamePolicy
{
    public const int MinimumLength = 8;
    public const int MaximumLength = 128;

    private static readonly string[] ReservedNames = ["Progmasoft", "Leitwolf"];

    public static PublisherNameValidation Validate(string? value)
    {
        string publisherName = value ?? string.Empty;
        if (publisherName.Length is < MinimumLength or > MaximumLength)
        {
            return PublisherNameValidation.Invalid(
                $"Username must be between {MinimumLength} and {MaximumLength} characters.");
        }
        if (!IsUpperAscii(publisherName[0]))
        {
            return PublisherNameValidation.Invalid("Username must start with an uppercase ASCII letter.");
        }

        // Publisher coordinates are URL path segments. Restricting the complete value to ASCII letters and digits keeps
        // canonical package URLs portable across filesystems, shells, registries, and case-sensitive clients.
        if (!publisherName.All(IsAsciiLetterOrDigit))
        {
            return PublisherNameValidation.Invalid("Username can contain only ASCII letters and digits.");
        }
        if (ReservedNames.Contains(publisherName, StringComparer.OrdinalIgnoreCase))
        {
            return PublisherNameValidation.Invalid("This username is reserved.");
        }

        return PublisherNameValidation.Valid(publisherName, publisherName.ToUpperInvariant());
    }

    private static bool IsUpperAscii(char value) => value is >= 'A' and <= 'Z';

    private static bool IsAsciiLetterOrDigit(char value) =>
        value is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9';
}

internal readonly record struct PublisherNameValidation(
    bool IsValid,
    string Value,
    string NormalizedValue,
    string? Error)
{
    public static PublisherNameValidation Valid(string value, string normalizedValue) =>
        new(true, value, normalizedValue, null);

    public static PublisherNameValidation Invalid(string error) => new(false, string.Empty, string.Empty, error);
}