Files
0qln bff34658b8 appendix: 23 Code-Ausschnitte aus dem Houston-Repository
Pin nixpkgs to the revision used by itc.componentware: the channel version
ships a latexminted that crashes on Python 3.14, which made every listing
fail and sent latexmk into an endless error prompt. Also force
nonstopmode/halt-on-error so a broken listing can never loop again.
2026-08-25 21:38:22 +02:00

113 lines
3.5 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Houston.Model.Documents;
public readonly record struct OrgSlug
{
public const int MaxLength = 128;
private const string CharactersToReplace = @"/\{}^%`][""><~#|";
private const char ReplacementCharacter = '-';
/// <summary>
/// Characters that must not start or end a folder name, because they are either invisible or
/// break path semantics on the client side.
/// </summary>
private static readonly char[] UntrimmableEdgeCharacters = [' ', '.', ReplacementCharacter];
public string Value { get; }
private OrgSlug(string value) => Value = value;
public static bool TryFromS3Key(string key, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
if (!key.EndsWith('/')) return false;
var parts = key.Split('/', StringSplitOptions.RemoveEmptyEntries);
return parts is [var name] && TryFromFolderName(name, out result);
}
public static bool TryFromFolderName(string name, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
if (String.IsNullOrWhiteSpace(name)) return false;
if (name.Contains('/')) return false;
if (name == "..") return false;
result = new OrgSlug(name);
return true;
}
/// <summary>
/// Builds the canonical folder name for an organization.
/// </summary>
/// <returns>
/// <c>false</c> if the name contains nothing that survives the normalization, in which case
/// there is no folder name Houston could safely claim.
/// </returns>
public static bool TryFrom(OrgName name, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
var slug = Normalize(name.Value, MaxLength);
if (slug.Length == 0)
return false;
result = new OrgSlug(slug);
return true;
}
public static bool TryFrom(OrgName name, OrgId id, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
var normalizedId = Normalize(id.Value, MaxLength);
if (normalizedId.Length == 0)
return false;
// The id is what makes the folder name unique, so the name is what gets truncated.
var suffix = $" ({normalizedId})";
if (suffix.Length >= MaxLength)
return false;
var slug = Normalize(name.Value, MaxLength - suffix.Length);
result = new OrgSlug(slug.Length == 0 ? normalizedId : slug + suffix);
return true;
}
private static string Normalize(string value, int maxLength)
{
var builder = new StringBuilder(Math.Min(value.Length, maxLength));
foreach (var character in value.Trim().TakeWhile(c => builder.Length != maxLength))
{
builder.Append(Char.IsControl(character) || CharactersToReplace.Contains(character)
? ReplacementCharacter
: character);
}
// Truncating can uncover a trailing space or dot, so trimming happens afterwards.
return builder.ToString().Trim().Trim(UntrimmableEdgeCharacters).Trim();
}
/// <summary>
/// S3 prefix of the folder, including the trailing slash.
/// </summary>
public override string ToString()
{
// A default(OrgSlug) would resolve to the bucket root and expose every customer's
// documents, so it must never be used as a prefix.
if (String.IsNullOrEmpty(Value))
throw new InvalidOperationException("An uninitialized OrgSlug cannot be used as an S3 prefix.");
return Value + '/';
}
}