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 68f9d68da..9479e31e3 100644 --- a/tests/Aetheria.Shared.Tests/CreditsTests.cs +++ b/tests/Aetheria.Shared.Tests/CreditsTests.cs @@ -37,9 +37,34 @@ 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", }; + // 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 @@ -140,12 +166,37 @@ 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(); + // 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. + 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}"); + // "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)); + } + } + private static System.Collections.Generic.Dictionary HeaderKeys() { var root = Root(); @@ -154,14 +205,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,10 +216,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)))); + // 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()); }