diff --git a/Wave.Tests/Data/ArticleTest.cs b/Wave.Tests/Data/ArticleTest.cs index 7063ee3..e726752 100644 --- a/Wave.Tests/Data/ArticleTest.cs +++ b/Wave.Tests/Data/ArticleTest.cs @@ -33,11 +33,32 @@ public class ArticleTest { Assert.That(Article.Slug, Is.EqualTo("article-title-that-is-longer-than-the-sixty-four-character-limit")); } + [Test] + public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition55() { + Article.Title = "Auto generating slugs was a mistake I hate this ______ €"; + Article.UpdateSlug(); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-______-%E2%82%AC")); + } + + [Test] + public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition56() { + Article.Title = "Auto generating slugs was a mistake I hate this _______ €"; + Article.UpdateSlug(); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-_______-")); + } + + [Test] + public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition57() { + Article.Title = "Auto generating slugs was a mistake I hate this ________ €"; + Article.UpdateSlug(); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-________-")); + } + [Test] public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition61() { Article.Title = "Article that ends with a special character and need special cäre"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-need-special-c%C3")); + Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-need-special-c")); } [Test] diff --git a/Wave/Data/Article.cs b/Wave/Data/Article.cs index 1cb9579..2bace2a 100644 --- a/Wave/Data/Article.cs +++ b/Wave/Data/Article.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; using Wave.Utilities; namespace Wave.Data; @@ -57,13 +58,21 @@ public class Article : ISoftDelete { string baseSlug = potentialNewSlug ?? Title; baseSlug = baseSlug.ToLowerInvariant()[..Math.Min(64, baseSlug.Length)]; string slug = Uri.EscapeDataString(baseSlug).Replace("-", "+").Replace("%20", "-"); - // if our escaping increases the slug length, there is a chance it ends with an escape - // character, so if this overshoot is not divisible by 3, then we risk cutting of the - // escape character, so we need to remove it in it's entirely if that's the case + + // I hate my life int escapeTrimOvershoot = 0; - if (slug.Length > 64) escapeTrimOvershoot = slug[62..64].Contains('%') ? 1 : 0; - // if the slug already fits 64 character, there will be no cutoff in the next operation anyway, - // so we don't need to fix what is described in the previous comment + if (slug.Length > 64) { + // Escape sequences come with a % and two hex digits, there may be up to 3 of such sequences + // per character escaping ('?' has %3F, but € has %E2%82%AC), so we need to find the last group + // of such an escape parade and see if it's going over by less than 9, because then we need to + // remove more characters in the truncation, or we end up with a partial escape sequence.. parade + escapeTrimOvershoot = 64 - Regex.Match(slug, + @"(?(%[a-fA-F\d][a-fA-F\d])+)", + RegexOptions.None | RegexOptions.ExplicitCapture) + .Groups.Values.Last(g => g.Index < 64).Index; + if (escapeTrimOvershoot > 9) escapeTrimOvershoot = 0; + } + Slug = slug[..Math.Min(slug.Length, 64 - escapeTrimOvershoot)]; if (Slug.EndsWith("%")) Slug = Slug[..^1]; }