소스 검색

Use RegexGenerator where possible

Bond_009 2 년 전
부모
커밋
b5f0760db8
25개의 변경된 파일137개의 추가작업 그리고 116개의 파일을 삭제
  1. 3 7
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  2. 6 7
      Emby.Naming/Audio/AlbumParser.cs
  3. 4 3
      Emby.Naming/TV/SeriesResolver.cs
  4. 8 4
      Emby.Naming/Video/VideoListResolver.cs
  5. 5 5
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  6. 5 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
  7. 6 3
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  8. 9 11
      Jellyfin.Server.Implementations/Users/UserManager.cs
  9. 6 7
      MediaBrowser.Common/Extensions/BaseExtensions.cs
  10. 5 2
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  11. 9 6
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  12. 7 4
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  13. 5 2
      MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs
  14. 5 2
      MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
  15. 5 2
      MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs
  16. 5 2
      MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
  17. 5 2
      MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
  18. 7 24
      MediaBrowser.Model/Dlna/SearchCriteria.cs
  19. 5 2
      MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
  20. 5 4
      MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
  21. 8 7
      MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
  22. 2 0
      jellyfin.ruleset
  23. 1 2
      src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
  24. 6 4
      src/Jellyfin.Extensions/StringExtensions.cs
  25. 5 2
      tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs

+ 3 - 7
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo
             _httpClientFactory = httpClientFactory;
         }
 
+        [GeneratedRegex("(&(?![a-z]*;))")]
+        private static partial Regex EscapeAmpersandRegex();
+
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
         {
             // If it's already a complete url, don't stick anything onto the front of it
@@ -128,12 +131,5 @@ namespace Emby.Dlna.PlayTo
             // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
             return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
         }
-
-        /// <summary>
-        /// Compile-time generated regular expression for escaping ampersands.
-        /// </summary>
-        /// <returns>Compiled regular expression.</returns>
-        [GeneratedRegex("(&(?![a-z]*;))")]
-        private static partial Regex EscapeAmpersandRegex();
     }
 }

+ 6 - 7
Emby.Naming/Audio/AlbumParser.cs

@@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
     /// <summary>
     /// Helper class to determine if Album is multipart.
     /// </summary>
-    public class AlbumParser
+    public partial class AlbumParser
     {
         private readonly NamingOptions _options;
 
@@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
             _options = options;
         }
 
+        [GeneratedRegex(@"([-\.\(\)]|\s+)")]
+        private static partial Regex CleanRegex();
+
         /// <summary>
         /// Function that determines if album is multipart.
         /// </summary>
@@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
 
             // Normalize
             // Remove whitespace
-            filename = filename.Replace('-', ' ');
-            filename = filename.Replace('.', ' ');
-            filename = filename.Replace('(', ' ');
-            filename = filename.Replace(')', ' ');
-            filename = Regex.Replace(filename, @"\s+", " ");
+            filename = CleanRegex().Replace(filename, " ");
 
-            ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
+            ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart();
 
             foreach (var prefix in _options.AlbumStackingPrefixes)
             {

+ 4 - 3
Emby.Naming/TV/SeriesResolver.cs

@@ -7,14 +7,15 @@ namespace Emby.Naming.TV
     /// <summary>
     /// Used to resolve information about series from path.
     /// </summary>
-    public static class SeriesResolver
+    public static partial class SeriesResolver
     {
         /// <summary>
         /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
         /// Used for removing separators between words, i.e turns "The_show" into "The show" while
         /// preserving namings like "S.H.O.W".
         /// </summary>
-        private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
+        [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
+        private static partial Regex SeriesNameRegex();
 
         /// <summary>
         /// Resolve information about series from path.
@@ -37,7 +38,7 @@ namespace Emby.Naming.TV
 
             if (!string.IsNullOrEmpty(seriesName))
             {
-                seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+                seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
             }
 
             return new SeriesInfo(path)

+ 8 - 4
Emby.Naming/Video/VideoListResolver.cs

@@ -12,9 +12,13 @@ namespace Emby.Naming.Video
     /// <summary>
     /// Resolves alternative versions and extras from list of video files.
     /// </summary>
-    public static class VideoListResolver
+    public static partial class VideoListResolver
     {
-        private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+        [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
+        private static partial Regex ResolutionRegex();
+
+        [GeneratedRegex(@"^\[([^]]*)\]")]
+        private static partial Regex CheckMultiVersionRegex();
 
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
@@ -131,7 +135,7 @@ namespace Emby.Naming.Video
 
             if (videos.Count > 1)
             {
-                var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+                var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
                 videos.Clear();
                 foreach (var group in groups)
                 {
@@ -201,7 +205,7 @@ namespace Emby.Naming.Video
             // The CleanStringParser should have removed common keywords etc.
             return testFilename.IsEmpty
                    || testFilename[0] == '-'
-                   || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+                   || CheckMultiVersionRegex().IsMatch(testFilename);
         }
     }
 }

+ 5 - 5
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
     /// <summary>
     /// Class MovieResolver.
     /// </summary>
-    public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
+    public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
     {
         private readonly IImageProcessor _imageProcessor;
 
@@ -56,6 +56,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         /// <value>The priority.</value>
         public override ResolverPriority Priority => ResolverPriority.Fourth;
 
+        [GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
+        private static partial Regex IsIgnoredRegex();
+
         /// <inheritdoc />
         public MultiItemResolverResult ResolveMultiple(
             Folder parent,
@@ -261,7 +264,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 {
                     leftOver.Add(child);
                 }
-                else if (!IsIgnored(child.Name))
+                else if (!IsIgnoredRegex().IsMatch(child.Name))
                 {
                     files.Add(child);
                 }
@@ -314,9 +317,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return result;
         }
 
-        private static bool IsIgnored(ReadOnlySpan<char> filename)
-            => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
         private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
         {
             for (var i = 0; i < result.Count; i++)

+ 5 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs

@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
-    public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+    public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
     {
         private string? _channel;
         private string? _program;
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public LegacyHdHomerunChannelCommands(string url)
         {
             // parse url for channel and program
-            var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
+            var match = ChannelAndProgramRegex().Match(url);
             if (match.Success)
             {
                 _channel = match.Groups[1].Value;
@@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
         }
 
+        [GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")]
+        private static partial Regex ChannelAndProgramRegex();
+
         public IEnumerable<(string CommandName, string CommandValue)> GetCommands()
         {
             if (!string.IsNullOrEmpty(_channel))

+ 6 - 3
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
-    public class M3uParser
+    public partial class M3uParser
     {
         private const string ExtInfPrefix = "#EXTINF:";
 
@@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             _httpClientFactory = httpClientFactory;
         }
 
+        [GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")]
+        private static partial Regex KeyValueRegex();
+
         public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
         {
             // Read the file and display it line by line.
@@ -311,7 +314,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
+            var matches = KeyValueRegex().Matches(line);
 
             remaining = line;
 
@@ -320,7 +323,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 var key = match.Groups[1].Value;
                 var value = match.Groups[2].Value;
 
-                dict[match.Groups[1].Value] = match.Groups[2].Value;
+                dict[key] = value;
                 remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase);
             }
 

+ 9 - 11
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -31,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Users
     /// <summary>
     /// Manages the creation and retrieval of <see cref="User"/> instances.
     /// </summary>
-    public class UserManager : IUserManager
+    public partial class UserManager : IUserManager
     {
         private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
         private readonly IEventManager _eventManager;
@@ -105,6 +105,12 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public IEnumerable<Guid> UsersIds => _users.Keys;
 
+        // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
+        // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
+        // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
+        [GeneratedRegex("^[\\w\\ \\-'._@]+$")]
+        private static partial Regex ValidUsernameRegex();
+
         /// <inheritdoc/>
         public User? GetUserById(Guid id)
         {
@@ -527,7 +533,7 @@ namespace Jellyfin.Server.Implementations.Users
             }
 
             var defaultName = Environment.UserName;
-            if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
+            if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
             {
                 defaultName = "MyJellyfinUser";
             }
@@ -710,7 +716,7 @@ namespace Jellyfin.Server.Implementations.Users
 
         internal static void ThrowIfInvalidUsername(string name)
         {
-            if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name))
+            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
             {
                 return;
             }
@@ -718,14 +724,6 @@ namespace Jellyfin.Server.Implementations.Users
             throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
         }
 
-        private static bool IsValidUsername(ReadOnlySpan<char> name)
-        {
-            // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
-            // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
-            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
-            return Regex.IsMatch(name, @"^[\w\ \-'._@]+$");
-        }
-
         private IAuthenticationProvider GetAuthenticationProvider(User user)
         {
             return GetAuthenticationProviders(user)[0];

+ 6 - 7
MediaBrowser.Common/Extensions/BaseExtensions.cs

@@ -8,20 +8,19 @@ namespace MediaBrowser.Common.Extensions
     /// <summary>
     /// Class BaseExtensions.
     /// </summary>
-    public static class BaseExtensions
+    public static partial class BaseExtensions
     {
+        // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
+        [GeneratedRegex(@"<(.|\n)*?>")]
+        private static partial Regex StripHtmlRegex();
+
         /// <summary>
         /// Strips the HTML.
         /// </summary>
         /// <param name="htmlString">The HTML string.</param>
         /// <returns><see cref="string" />.</returns>
         public static string StripHtml(this string htmlString)
-        {
-            // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
-            const string Pattern = @"<(.|\n)*?>";
-
-            return Regex.Replace(htmlString, Pattern, string.Empty).Trim();
-        }
+            => StripHtmlRegex().Replace(htmlString, string.Empty).Trim();
 
         /// <summary>
         /// Gets the Md5.

+ 5 - 2
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -23,7 +23,7 @@ using Microsoft.Extensions.Configuration;
 
 namespace MediaBrowser.Controller.MediaEncoding
 {
-    public class EncodingHelper
+    public partial class EncodingHelper
     {
         private const string QsvAlias = "qs";
         private const string VaapiAlias = "va";
@@ -112,6 +112,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             _config = config;
         }
 
+        [GeneratedRegex(@"\s+")]
+        private static partial Regex WhiteSpaceRegex();
+
         public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
             => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions);
 
@@ -1733,7 +1736,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
-            profile = Regex.Replace(profile, @"\s+", string.Empty);
+            profile = WhiteSpaceRegex().Replace(profile, string.Empty);
 
             // We only transcode to HEVC 8-bit for now, force Main Profile.
             if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)

+ 9 - 6
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.MediaEncoding.Encoder
 {
-    public class EncoderValidator
+    public partial class EncoderValidator
     {
         private static readonly string[] _requiredDecoders = new[]
         {
@@ -160,6 +160,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         public static Version? MaxVersion { get; } = null;
 
+        [GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")]
+        private static partial Regex FfmpegVersionRegex();
+
+        [GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)]
+        private static partial Regex LibraryRegex();
+
         public bool ValidateVersion()
         {
             string output;
@@ -278,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         internal Version? GetFFmpegVersionInternal(string output)
         {
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
-            var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
+            var match = FfmpegVersionRegex().Match(output);
 
             if (match.Success)
             {
@@ -326,10 +332,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
             var map = new Dictionary<string, Version>();
 
-            foreach (Match match in Regex.Matches(
-                output,
-                @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
-                RegexOptions.Multiline))
+            foreach (Match match in LibraryRegex().Matches(output))
             {
                 var version = new Version(
                     int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),

+ 7 - 4
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -36,7 +36,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
     /// <summary>
     /// Class MediaEncoder.
     /// </summary>
-    public class MediaEncoder : IMediaEncoder, IDisposable
+    public partial class MediaEncoder : IMediaEncoder, IDisposable
     {
         /// <summary>
         /// The default SDR image extraction timeout in milliseconds.
@@ -142,6 +142,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <inheritdoc />
         public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
 
+        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
+        private static partial Regex FfprobePathRegex();
+
         /// <summary>
         /// Run at startup or if the user removes a Custom path from transcode page.
         /// Sets global variables FFmpegPath.
@@ -176,7 +179,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             if (_ffmpegPath is not null)
             {
                 // Determine a probe path from the mpeg path
-                _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
+                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, @"ffprobe$1");
 
                 // Interrogate to understand what coders are supported
                 var validator = new EncoderValidator(_logger, _ffmpegPath);
@@ -416,8 +419,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
         {
             var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
-            string analyzeDuration = string.Empty;
-            string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
+            var analyzeDuration = string.Empty;
+            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 
             if (request.MediaSource.AnalyzeDurationMs > 0)
             {

+ 5 - 2
MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs

@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     /// <summary>
     /// ASS subtitle writer.
     /// </summary>
-    public class AssWriter : ISubtitleWriter
+    public partial class AssWriter : ISubtitleWriter
     {
+        [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
+        private static partial Regex NewLineRegex();
+
         /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
@@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     var trackEvent = trackEvents[i];
                     var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
                     var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
-                    var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
+                    var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
 
                     writer.WriteLine(
                         "Dialogue: 0,{0},{1},Default,{2}",

+ 5 - 2
MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs

@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     /// <summary>
     /// SRT subtitle writer.
     /// </summary>
-    public class SrtWriter : ISubtitleWriter
+    public partial class SrtWriter : ISubtitleWriter
     {
+        [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
+        private static partial Regex NewLineEscapedRegex();
+
         /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
@@ -35,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     var text = trackEvent.Text;
 
                     // TODO: Not sure how to handle these
-                    text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
+                    text = NewLineEscapedRegex().Replace(text, " ");
 
                     writer.WriteLine(text);
                     writer.WriteLine();

+ 5 - 2
MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs

@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     /// <summary>
     /// SSA subtitle writer.
     /// </summary>
-    public class SsaWriter : ISubtitleWriter
+    public partial class SsaWriter : ISubtitleWriter
     {
+        [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
+        private static partial Regex NewLineRegex();
+
         /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
@@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     var trackEvent = trackEvents[i];
                     var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
                     var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
-                    var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
+                    var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
 
                     writer.WriteLine(
                         "Dialogue: 0,{0},{1},Default,{2}",

+ 5 - 2
MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs

@@ -9,8 +9,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     /// <summary>
     /// TTML subtitle writer.
     /// </summary>
-    public class TtmlWriter : ISubtitleWriter
+    public partial class TtmlWriter : ISubtitleWriter
     {
+        [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
+        private static partial Regex NewLineEscapeRegex();
+
         /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
@@ -38,7 +41,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 {
                     var text = trackEvent.Text;
 
-                    text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase);
+                    text = NewLineEscapeRegex().Replace(text, "<br/>");
 
                     writer.WriteLine(
                         "<p begin=\"{0}\" dur=\"{1}\">{2}</p>",

+ 5 - 2
MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs

@@ -10,8 +10,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     /// <summary>
     /// Subtitle writer for the WebVTT format.
     /// </summary>
-    public class VttWriter : ISubtitleWriter
+    public partial class VttWriter : ISubtitleWriter
     {
+        [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
+        private static partial Regex NewlineEscapeRegex();
+
         /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
@@ -39,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     var text = trackEvent.Text;
 
                     // TODO: Not sure how to handle these
-                    text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
+                    text = NewlineEscapeRegex().Replace(text, " ");
 
                     writer.WriteLine(text);
                     writer.WriteLine();

+ 7 - 24
MediaBrowser.Model/Dlna/SearchCriteria.cs

@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
 
 namespace MediaBrowser.Model.Dlna
 {
-    public class SearchCriteria
+    public partial class SearchCriteria
     {
         public SearchCriteria(string search)
         {
@@ -13,10 +13,10 @@ namespace MediaBrowser.Model.Dlna
 
             SearchType = SearchType.Unknown;
 
-            string[] factors = RegexSplit(search, "(and|or)");
+            string[] factors = AndOrRegex().Split(search);
             foreach (string factor in factors)
             {
-                string[] subFactors = RegexSplit(factor.Trim().Trim('(').Trim(')').Trim(), "\\s", 3);
+                string[] subFactors = WhiteSpaceRegex().Split(factor.Trim().Trim('(').Trim(')').Trim(), 3);
 
                 if (subFactors.Length == 3)
                 {
@@ -46,27 +46,10 @@ namespace MediaBrowser.Model.Dlna
 
         public SearchType SearchType { get; set; }
 
-        /// <summary>
-        /// Splits the specified string.
-        /// </summary>
-        /// <param name="str">The string.</param>
-        /// <param name="term">The term.</param>
-        /// <param name="limit">The limit.</param>
-        /// <returns>System.String[].</returns>
-        private static string[] RegexSplit(string str, string term, int limit)
-        {
-            return new Regex(term).Split(str, limit);
-        }
+        [GeneratedRegex("\\s")]
+        private static partial Regex WhiteSpaceRegex();
 
-        /// <summary>
-        /// Splits the specified string.
-        /// </summary>
-        /// <param name="str">The string.</param>
-        /// <param name="term">The term.</param>
-        /// <returns>System.String[].</returns>
-        private static string[] RegexSplit(string str, string term)
-        {
-            return Regex.Split(str, term, RegexOptions.IgnoreCase);
-        }
+        [GeneratedRegex("(and|or)", RegexOptions.IgnoreCase)]
+        private static partial Regex AndOrRegex();
     }
 }

+ 5 - 2
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.MediaInfo
     /// <summary>
     /// Probes audio files for metadata.
     /// </summary>
-    public class AudioFileProber
+    public partial class AudioFileProber
     {
         // Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain).
         private const float DefaultLUFSValue = -18;
@@ -58,6 +58,9 @@ namespace MediaBrowser.Providers.MediaInfo
             _mediaSourceManager = mediaSourceManager;
         }
 
+        [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")]
+        private static partial Regex LUFSRegex();
+
         /// <summary>
         /// Probes the specified item for metadata.
         /// </summary>
@@ -129,7 +132,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                     output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
                     cancellationToken.ThrowIfCancellationRequested();
-                    MatchCollection split = Regex.Matches(output, @"I:\s+(.*?)\s+LUFS");
+                    MatchCollection split = LUFSRegex().Matches(output);
 
                     if (split.Count != 0)
                     {

+ 5 - 4
MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs

@@ -11,10 +11,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
     /// <summary>
     /// Utilities for the TMDb provider.
     /// </summary>
-    public static class TmdbUtils
+    public static partial class TmdbUtils
     {
-        private static readonly Regex _nonWords = new(@"[\W_]+", RegexOptions.Compiled);
-
         /// <summary>
         /// URL of the TMDb instance to use.
         /// </summary>
@@ -50,6 +48,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
             PersonKind.Producer
         };
 
+        [GeneratedRegex(@"[\W_]+")]
+        private static partial Regex NonWordRegex();
+
         /// <summary>
         /// Cleans the name according to TMDb requirements.
         /// </summary>
@@ -58,7 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         public static string CleanName(string name)
         {
             // TMDb expects a space separated list of words make sure that is the case
-            return _nonWords.Replace(name, " ");
+            return NonWordRegex().Replace(name, " ");
         }
 
         /// <summary>

+ 8 - 7
MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

@@ -27,15 +27,12 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.XbmcMetadata.Savers
 {
-    public abstract class BaseNfoSaver : IMetadataFileSaver
+    public abstract partial class BaseNfoSaver : IMetadataFileSaver
     {
         public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
 
         public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v=";
 
-        // filters control characters but allows only properly-formed surrogate sequences
-        private const string _invalidXMLCharsRegex = @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]";
-
         private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
             "plot",
@@ -148,6 +145,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
         public static string SaverName => "Nfo";
 
+        // filters control characters but allows only properly-formed surrogate sequences
+        // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify
+        // Web Archive version of link since it's not really explained in the thread.
+        [GeneratedRegex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]")]
+        private static partial Regex InvalidXMLCharsRegexRegex();
+
         /// <inheritdoc />
         public string GetSavePath(BaseItem item)
             => GetLocalSavePath(item);
@@ -354,9 +357,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
                 if (!string.IsNullOrEmpty(stream.Language))
                 {
-                    // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify
-                    // Web Archive version of link since it's not really explained in the thread.
-                    writer.WriteElementString("language", Regex.Replace(stream.Language, _invalidXMLCharsRegex, string.Empty));
+                    writer.WriteElementString("language", InvalidXMLCharsRegexRegex().Replace(stream.Language, string.Empty));
                 }
 
                 var scanType = stream.IsInterlaced ? "interlaced" : "progressive";

+ 2 - 0
jellyfin.ruleset

@@ -60,6 +60,8 @@
     <Rule Id="SA1515" Action="None" />
     <!-- disable warning SA1600: Elements should be documented -->
     <Rule Id="SA1600" Action="None" />
+    <!-- disable warning SA1601: Partial elements should be documented -->
+    <Rule Id="SA1601" Action="None" />
     <!-- disable warning SA1602: Enumeration items should be documented -->
     <Rule Id="SA1602" Action="None" />
     <!-- disable warning SA1633: The file header is missing or not located at the top of the file -->

+ 1 - 2
src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -123,8 +123,7 @@ public partial class StripCollageBuilder
         var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
 
         // use the system fallback to find a typeface for the given CJK character
-        var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
-        var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
+        var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty);
         if (!string.IsNullOrEmpty(filteredName))
         {
             typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);

+ 6 - 4
src/Jellyfin.Extensions/StringExtensions.cs

@@ -6,11 +6,13 @@ namespace Jellyfin.Extensions
     /// <summary>
     /// Provides extensions methods for <see cref="string" />.
     /// </summary>
-    public static class StringExtensions
+    public static partial class StringExtensions
     {
         // Matches non-conforming unicode chars
         // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
-        private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)", RegexOptions.Compiled);
+
+        [GeneratedRegex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(�)")]
+        private static partial Regex NonConformingUnicodeRegex();
 
         /// <summary>
         /// Removes the diacritics character from the strings.
@@ -19,7 +21,7 @@ namespace Jellyfin.Extensions
         /// <returns>The string without diacritics character.</returns>
         public static string RemoveDiacritics(this string text)
             => Diacritics.Extensions.StringExtensions.RemoveDiacritics(
-                _nonConformingUnicode.Replace(text, string.Empty));
+                NonConformingUnicodeRegex().Replace(text, string.Empty));
 
         /// <summary>
         /// Checks whether or not the specified string has diacritics in it.
@@ -28,7 +30,7 @@ namespace Jellyfin.Extensions
         /// <returns>True if the string has diacritics, false otherwise.</returns>
         public static bool HasDiacritics(this string text)
             => Diacritics.Extensions.StringExtensions.HasDiacritics(text)
-                || _nonConformingUnicode.IsMatch(text);
+                || NonConformingUnicodeRegex().IsMatch(text);
 
         /// <summary>
         /// Counts the number of occurrences of [needle] in the string.

+ 5 - 2
tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs

@@ -25,10 +25,13 @@ using Xunit;
 
 namespace Jellyfin.Providers.Tests.Manager
 {
-    public class ItemImageProviderTests
+    public partial class ItemImageProviderTests
     {
         private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
 
+        [GeneratedRegex("[0-9]+")]
+        private static partial Regex NumbersRegex();
+
         [Fact]
         public void ValidateImages_PhotoEmptyProviders_NoChange()
         {
@@ -463,7 +466,7 @@ namespace Jellyfin.Providers.Tests.Manager
             // images from the provider manager are sorted by preference (earlier images are higher priority) so we can verify that low url numbers are chosen
             foreach (var image in actualImages)
             {
-                var index = int.Parse(Regex.Match(image.Path, @"[0-9]+").Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
+                var index = int.Parse(NumbersRegex().Match(image.Path).ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture);
                 Assert.True(index < imageCount);
             }
         }