From 2071cea6e727a4e6ae1bb467f11b4772508ad662 Mon Sep 17 00:00:00 2001 From: Nibu Date: Thu, 12 Nov 2026 16:32:03 +0000 Subject: [PATCH 1/3] Credits test: header scan catches (c) and copyright-sign notices, lowercase copyright, 'Copyright:', every holder on a line, whole holder compared Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/CreditsTests.cs | 43 ++++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/CreditsTests.cs b/tests/Aetheria.Shared.Tests/CreditsTests.cs index 68f9d68da..acce8d59e 100644 --- a/tests/Aetheria.Shared.Tests/CreditsTests.cs +++ b/tests/Aetheria.Shared.Tests/CreditsTests.cs @@ -37,7 +37,7 @@ namespace Aetheria.Shared.Tests // Copyright lines found in source headers whose holder no credit shows, each with the reason; keyed like HeaderKeys. private static readonly System.Collections.Generic.Dictionary ReviewedHeaders = new System.Collections.Generic.Dictionary { - ["microsoftopen"] = "UniRx files derived from Rx.NET: the header names Microsoft Open Technologies and points at a License.txt that is not in the repo, so no licence is stated; decision owed", + ["microsoftopentechnologiesinc"] = "UniRx files derived from Rx.NET: the header names Microsoft Open Technologies and points at a License.txt that is not in the repo, so no licence is stated; decision owed", }; // Bundled works with no licence-like file at all, found by reading Assets. Their licences are not stated in the @@ -140,12 +140,30 @@ namespace Aetheria.Shared.Tests private static string[] Unaccounted(string[] found, System.Collections.Generic.IEnumerable credited) => found.Where(path => !credited.Contains(path) && !ReviewedNotCredited.ContainsKey(path)).ToArray(); - // The holder keys of every "Copyright ..." line in the source files under Assets: the year (when there is one) and - // the first two words of the holder, letters and digits only, lower case. Credits.cs is the credit text itself. - private static readonly Regex HeaderCopyright = new Regex(@"Copyright\s*(?:\([cC]\))?\s*(?:by\s+)?[<\[]?((?:\d{4}|[A-Z])[^\r\n]*)", RegexOptions.CultureInvariant); + // Every copyright marker in a source header: "Copyright", "copyright", "(c)" (not the tail of an identifier), the copyright sign, "Copyright (c)", with an optional + // colon and "by". A line is scanned marker by marker: a holder runs to the next marker or the end of the line, so a + // second holder on the same line is a holder of its own. Credits.cs is the credit text itself, so it is not scanned. + private const string Marker = @"(?:copyright(?:\s*(?:\(c\)|\u00a9))?|(?\s*:)?\s*(?:by\s+)?[<\[]?(?.+?)(?=" + Marker + @"|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static string Key(string text) => new string(text.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant(); + // The holders a line of source states: the text after each marker, cut at the end of its sentence, as keys. A holder + // is a year or a capitalised name; "copyright notice" in licence prose names nobody. Without a year the marker must + // be "(c)", the copyright sign or a colon, so "Copyright Notice" prose is not a holder either. + private static System.Collections.Generic.IEnumerable LineHolders(string line) + { + foreach (Match match in HeaderCopyright.Matches(line)) + { + var holder = Regex.Replace(match.Groups["holder"].Value.Trim(), @"(\.(\s.*)?|\s*All rights reserved.*)$", "", RegexOptions.IgnoreCase).Trim(' ', '\t', ',', ';', '>', ']'); + var marked = Regex.IsMatch(match.Value.Substring(0, match.Groups["holder"].Index - match.Index), @"\(c\)|\u00a9|:", RegexOptions.IgnoreCase); + var year = Regex.IsMatch(holder, @"^\d{4}"); + if (holder.Length == 0 || !(year || (marked && char.IsUpper(holder[0])))) continue; + yield return Key(Normal(holder)); + } + } + private static System.Collections.Generic.Dictionary HeaderKeys() { var root = Root(); @@ -154,14 +172,8 @@ namespace Aetheria.Shared.Tests .Select(path => Path.GetRelativePath(root, path).Replace('\\', '/')) .Where(path => new[] { ".shader", ".hlsl", ".cginc", ".cs", ".compute" }.Any(ext => path.EndsWith(ext)) && path != "Assets/Scripts/ServerShared/Credits.cs")) foreach (var line in Read(path).Split('\n')) - foreach (Match match in HeaderCopyright.Matches(line)) - { - var words = Regex.Replace(Regex.Replace(match.Groups[1].Value, @"\.\s.*", ""), @"[<>,]", " ").Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Where(word => word != "by").ToArray(); - var year = words.Length > 0 && Regex.IsMatch(words[0], @"^\d{4}") ? 1 : 0; - if (year == 0 && !Regex.IsMatch(line, @"\([cC]\)")) continue; - keys.TryAdd(Key(string.Concat(words.Take(year + 2))), path); - } + foreach (var key in LineHolders(line)) + keys.TryAdd(key, path); return keys; } @@ -171,11 +183,12 @@ namespace Aetheria.Shared.Tests public void EveryCopyrightHolderInASourceHeaderIsCreditedOrReviewed() { var keys = HeaderKeys(); - Assert.Contains("2015academyof", keys.Keys); + Assert.Contains(Key("2015 Academy of Motion Picture Arts and Sciences (A.M.P.A.S"), keys.Keys); Assert.Contains("2016unitytechnologies", keys.Keys); Assert.Contains("2017supyrb", keys.Keys); - var shown = Key(string.Join(" ", Load().Select(credit => Normal(credit.Notice)))); - Assert.Equal(Array.Empty(), keys.Where(k => !shown.Contains(k.Key) && !ReviewedHeaders.ContainsKey(k.Key)).Select(k => $"{k.Key} in {k.Value}").ToArray()); + // The whole holder is compared, not a prefix of it, against each credit's notice on its own. + var shown = Load().Select(credit => Key(Normal(credit.Notice))).ToArray(); + Assert.Equal(Array.Empty(), keys.Where(k => !shown.Any(notice => notice.Contains(k.Key)) && !ReviewedHeaders.ContainsKey(k.Key)).Select(k => $"{k.Key} in {k.Value}").ToArray()); Assert.Equal(Array.Empty(), ReviewedHeaders.Keys.Where(k => !keys.ContainsKey(k)).ToArray()); } -- 2.47.3 From 50ab1cd7f48e698ee60abc58c67cbe271c693b7a Mon Sep 17 00:00:00 2001 From: Nibu Date: Thu, 12 Nov 2026 16:33:18 +0000 Subject: [PATCH 2/3] Credits: credit uiextensions' BezierPath (Tulleken); review the credit-only lines in StableFluids, Triplanar and uiextensions Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Credits.cs | 9 +++++++ tests/Aetheria.Shared.Tests/CreditsTests.cs | 26 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Assets/Scripts/ServerShared/Credits.cs b/Assets/Scripts/ServerShared/Credits.cs index 45054187b..9b7ac4226 100644 --- a/Assets/Scripts/ServerShared/Credits.cs +++ b/Assets/Scripts/ServerShared/Credits.cs @@ -31,6 +31,7 @@ public static class Credits public const string UnityShaderFile = "Assets/Shaders/Particle Standard Unlit.shader"; public const string AdditiveUiFile = "Assets/Shaders/AdditiveUI.shader"; public const string AcesFile = "Assets/Shaders/CustomDoF/ACES.hlsl"; + public const string BezierPathFile = "Assets/Plugins/com.unity.uiextensions/Runtime/Scripts/Utilities/BezierPath.cs"; public const string ClassicNoiseFile = "Assets/Plugins/Celestial Body/Scripts/Shaders/Includes/SimplexNoise2.cginc"; // The first line of the attribution file; the icon credits are the lines after it. public const string NounProjectHeading = "Noun Project"; @@ -138,6 +139,14 @@ public static class Credits LicenceFile = AcesFile }, new Credit + { + Name = "Bezier path (UI Extensions)", + Licence = "No named licence; permissive grant in the file header", + Notice = "Author: Herman Tulleken, dev.mag.org.za (http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/). " + + "Use this code as you wish, at your own risk.", + LicenceFile = BezierPathFile + }, + new Credit { Name = "ambientCG PBR materials", Licence = "CC0 1.0 Universal", diff --git a/tests/Aetheria.Shared.Tests/CreditsTests.cs b/tests/Aetheria.Shared.Tests/CreditsTests.cs index acce8d59e..2ef34f354 100644 --- a/tests/Aetheria.Shared.Tests/CreditsTests.cs +++ b/tests/Aetheria.Shared.Tests/CreditsTests.cs @@ -40,6 +40,31 @@ namespace Aetheria.Shared.Tests ["microsoftopentechnologiesinc"] = "UniRx files derived from Rx.NET: the header names Microsoft Open Technologies and points at a License.txt that is not in the repo, so no licence is stated; decision owed", }; + // Source headers that credit someone ("Credit X", "Based on ...", a source URL) without stating a copyright holder or + // a licence, so there is no true notice to show. Each is the exact line the file carries and the reason it is not a + // credit; the test reads the file, so a line that moves or goes cannot leave a stale entry. + private static readonly (string file, string line, string reason)[] ReviewedAttributionLines = + { + ("Assets/Shaders/StableFluids/Fluid.cs", "// https://github.com/keijiro/StableFluids", "names Keijiro's StableFluids as the source; states no holder and no licence"), + ("Assets/Shaders/StableFluids/Fluid.shader", "// https://github.com/keijiro/StableFluids", "names Keijiro's StableFluids as the source; states no holder and no licence"), + ("Assets/Shaders/StableFluids/Fluid.compute", "// Based on https://github.com/keijiro/StableFluids", "names Keijiro's StableFluids as the source; states no holder and no licence"), + ("Assets/Plugins/Celestial Body/Scripts/Shaders/Includes/Triplanar.cginc", "// Based on: medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a", "cites bgolus's article for the technique; states no holder and no licence"), + ("Assets/Plugins/com.unity.uiextensions/Runtime/Scripts/Primitives/UILineTextureRenderer.cs", "/// Credit jonbro5556", "names a contributor; states no copyright and no licence"), + ("Assets/Plugins/com.unity.uiextensions/Runtime/Scripts/Primitives/UILineRenderer.cs", "/// Credit jack.sydorenko, firagon", "names contributors; states no copyright and no licence"), + ("Assets/Plugins/com.unity.uiextensions/Runtime/Scripts/Primitives/UILineRendererList.cs", "/// Credit jack.sydorenko, firagon", "names contributors; states no copyright and no licence"), + ("Assets/Plugins/com.unity.uiextensions/Runtime/Scripts/Utilities/CableCurve.cs", "/// Credit Farfarer", "names a contributor; states no copyright and no licence"), + ("Assets/Plugins/com.unity.uiextensions/Editor/BezierLineRendererEditor.cs", "///Credit Jason Horsburgh", "names a contributor; states no copyright and no licence"), + }; + + [Fact] + public void ReviewedAttributionLinesAreStillInTheirFiles() + { + Assert.Equal(Array.Empty(), ReviewedAttributionLines + .Where(entry => !File.Exists(Path.Combine(Root(), entry.file)) || !Read(entry.file).Replace("\r", "").Split('\n').Any(line => line.Trim('\uFEFF', ' ', '\t') == entry.line)) + .Select(entry => $"{entry.line} in {entry.file}").ToArray()); + Assert.All(ReviewedAttributionLines, entry => Assert.False(string.IsNullOrWhiteSpace(entry.reason))); + } + // Bundled works with no licence-like file at all, found by reading Assets. Their licences are not stated in the // repo, so they are not credited; each path must still exist, so this list cannot go stale unnoticed. private static readonly string[] UnlicensedWorks = @@ -65,6 +90,7 @@ namespace Aetheria.Shared.Tests (Credits.AdditiveUiFile, new[] { "Unity Technologies", "Supyrb", "Johannes Deml" }), (Credits.AcesFile, new[] { "Academy of Motion Picture Arts and Sciences", "A.M.P.A.S." }), (Credits.ClassicNoiseFile, new[] { "Ashima Arts", "Stefan Gustavson" }), + (Credits.BezierPathFile, new[] { "Herman Tulleken", "Use this code as you wish" }), }; // Fonts that state holder and licence in their own name tables: the credit's LicenceFile is the licence text shipped -- 2.47.3 From 61cea7d8c7a5fe2e8a209bca0a17c33ea355c806 Mon Sep 17 00:00:00 2001 From: Nibu Date: Thu, 12 Nov 2026 16:52:06 +0000 Subject: [PATCH 3/3] Credits test: header holders must equal a credited holder; scan Copyright followed by a bare name Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/CreditsTests.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/CreditsTests.cs b/tests/Aetheria.Shared.Tests/CreditsTests.cs index 2ef34f354..9479e31e3 100644 --- a/tests/Aetheria.Shared.Tests/CreditsTests.cs +++ b/tests/Aetheria.Shared.Tests/CreditsTests.cs @@ -175,6 +175,11 @@ namespace Aetheria.Shared.Tests private static string Key(string text) => new string(text.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant(); + // Words that follow "copyright" in licence and header prose and name no holder ("copyright notice", "COPYRIGHT HOLDERS BE + // LIABLE", "copyright laws"); a capitalised word after a bare "Copyright" is a holder unless it is one of these. + private static readonly string[] ProseAfterCopyright = + { "Notice", "Notices", "Holder", "Holders", "Owner", "Owners", "Law", "Laws", "Act", "Protection", "Statement", "Header", "Information", "Status", "Line", "Lines", "Text", "Licence", "License", "Material", "Works", "Work", "File", "Files" }; + // The holders a line of source states: the text after each marker, cut at the end of its sentence, as keys. A holder // is a year or a capitalised name; "copyright notice" in licence prose names nobody. Without a year the marker must // be "(c)", the copyright sign or a colon, so "Copyright Notice" prose is not a holder either. @@ -185,7 +190,9 @@ namespace Aetheria.Shared.Tests var holder = Regex.Replace(match.Groups["holder"].Value.Trim(), @"(\.(\s.*)?|\s*All rights reserved.*)$", "", RegexOptions.IgnoreCase).Trim(' ', '\t', ',', ';', '>', ']'); var marked = Regex.IsMatch(match.Value.Substring(0, match.Groups["holder"].Index - match.Index), @"\(c\)|\u00a9|:", RegexOptions.IgnoreCase); var year = Regex.IsMatch(holder, @"^\d{4}"); - if (holder.Length == 0 || !(year || (marked && char.IsUpper(holder[0])))) continue; + // "Copyright Epic Games, Inc." names a holder with no year or sign; licence prose after the word does not. + var named = holder.Length > 0 && char.IsUpper(holder[0]) && !ProseAfterCopyright.Contains(Regex.Match(holder, @"^\w+").Value, StringComparer.OrdinalIgnoreCase); + if (holder.Length == 0 || !(year || (named && (marked || Regex.IsMatch(match.Value, "^copyright", RegexOptions.IgnoreCase))))) continue; yield return Key(Normal(holder)); } } @@ -212,9 +219,10 @@ namespace Aetheria.Shared.Tests Assert.Contains(Key("2015 Academy of Motion Picture Arts and Sciences (A.M.P.A.S"), keys.Keys); Assert.Contains("2016unitytechnologies", keys.Keys); Assert.Contains("2017supyrb", keys.Keys); - // The whole holder is compared, not a prefix of it, against each credit's notice on its own. - var shown = Load().Select(credit => Key(Normal(credit.Notice))).ToArray(); - Assert.Equal(Array.Empty(), keys.Where(k => !shown.Any(notice => notice.Contains(k.Key)) && !ReviewedHeaders.ContainsKey(k.Key)).Select(k => $"{k.Key} in {k.Value}").ToArray()); + // The whole holder must equal a holder some credit's notice states, read by the same extractor: a bare year, a short + // name, or a prefix of a credited name is a different key and fails. + var shown = Load().SelectMany(credit => credit.Notice.Split('\n')).SelectMany(LineHolders).ToHashSet(); + Assert.Equal(Array.Empty(), keys.Where(k => !shown.Contains(k.Key) && !ReviewedHeaders.ContainsKey(k.Key)).Select(k => $"{k.Key} in {k.Value}").ToArray()); Assert.Equal(Array.Empty(), ReviewedHeaders.Keys.Where(k => !keys.ContainsKey(k)).ToArray()); } -- 2.47.3