diff options
25 files changed, 137 insertions, 116 deletions
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 8b983e9e3..8454c1afd 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/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(); } } diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 86a564153..73424a134 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/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) { diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index 307a84096..d8fa41743 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/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) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 6209cd46f..51f29cf08 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/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); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index ea980b992..0b65bf921 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/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++) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 3450f971f..654474e97 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/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)) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index b41816230..df9101f48 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/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); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 1d03baa4c..ec0c64cd7 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/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]; diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index e3775021e..3615b662b 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/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. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674d..89cf55b78 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/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) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d3843796f..0e493afb4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/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), diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4e63d205c..0885fbe92 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/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) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs index 0d1cf6e25..7d7b80e99 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs +++ b/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}", diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 143c010b7..86f77aa06 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/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(); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs index 6761cd309..b5fd1ed93 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs +++ b/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}", diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index e5c785bc5..ea45f2070 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/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>", diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index 38ef57dee..3e0f47b5a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/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(); diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs index 77d6a55ea..6f4a692c8 100644 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ b/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(); } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index e1dcbc993..a7e8f774c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/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) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 516eee758..a7c93ac4c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/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> diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 4f8f869ac..bf66a3145 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/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"; diff --git a/jellyfin.ruleset b/jellyfin.ruleset index b611caa11..c846e2cd4 100644 --- a/jellyfin.ruleset +++ b/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 --> diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index a7a3338df..537fbf236 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/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]); diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index b22eb7c4e..fd8f7e59a 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/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. diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 925e8fa19..f157f01e5 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/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); } } |
