aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2026-08-20 18:10:50 -0400
committerCody Robibero <cody@robibe.ro>2026-08-20 18:10:50 -0400
commit9ba91d4583f3671eaacae8e073fb53288f9b8715 (patch)
treef17354faa2c854d4b6f46b26d9e8517675565f22
parenta8da0664a387aa871f7c0ee03fd3f53c82d00346 (diff)
Normalize fix, apply in more places
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs5
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs17
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs8
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs37
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs4
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs23
9 files changed, 76 insertions, 29 deletions
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index e4939205c9..29b633530f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -174,7 +174,7 @@ public partial class AudioNormalizationTask : IScheduledTask
if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
{
t.LUFS = await CalculateLUFSAsync(
- string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
+ string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.EscapeProcessArgument()),
false,
cancellationToken).ConfigureAwait(false);
toSaveDbItems.Add(t);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index a6555a2beb..034a9dea55 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -20,7 +20,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
-using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -1652,9 +1651,9 @@ public class DynamicHlsController : BaseJellyfinApiController
segmentFormat,
startNumber.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
- EncodingUtils.NormalizePath(outputTsArg),
+ outputTsArg.EscapeProcessArgument(),
hlsArguments,
- EncodingUtils.NormalizePath(outputPath)).Trim();
+ outputPath.EscapeProcessArgument()).Trim();
}
/// <summary>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 57c130fa4b..10c21ee03c 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1318,7 +1318,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(canvasArgs);
}
- arg.Append(" -i file:\"").Append(subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)).Append('\"');
+ arg.Append(" -i file:\"").Append(subtitlePath.EscapeProcessArgument()).Append('\"');
}
if (state.AudioStream is not null && state.AudioStream.IsExternal)
@@ -1330,7 +1330,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(' ').Append(seekAudioParam);
}
- arg.Append(" -i \"").Append(state.AudioStream.Path.Replace("\"", "\\\"", StringComparison.Ordinal)).Append('"');
+ arg.Append(" -i \"").Append(state.AudioStream.Path.EscapeProcessArgument()).Append('"');
}
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 12a5ab877c..fbe8afc66e 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -160,7 +159,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
CultureInfo.InvariantCulture,
"-dump_attachment:{0} \"{1}\" ",
attachment.Index,
- EncodingUtils.NormalizePath(attachmentPath));
+ attachmentPath.EscapeProcessArgument());
missingPaths.Add(attachmentPath);
}
@@ -425,7 +424,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
"-dump_attachment:{1} \"{2}\" -i {0} {3}",
inputPath,
attachmentStreamIndex,
- EncodingUtils.NormalizePath(outputPath),
+ outputPath.EscapeProcessArgument(),
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
int exitCode;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index 2daeac7343..a525dcfa62 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -42,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
- var files = string.Join('|', inputFiles.Select(NormalizePath));
+ var files = string.Join('|', inputFiles.Select(f => f.EscapeProcessArgument()));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
@@ -64,21 +65,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path);
}
- // Quotes are valid path characters in linux and they need to be escaped here with a leading \
- path = NormalizePath(path);
+ path = path.EscapeProcessArgument();
return string.Format(CultureInfo.InvariantCulture, "{1}:\"{0}\"", path, inputPrefix);
}
-
- /// <summary>
- /// Normalizes the path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>System.String.</returns>
- public static string NormalizePath(string path)
- {
- // Quotes are valid path characters in linux and they need to be escaped here with a leading \
- return path.Replace("\"", "\\\"", StringComparison.Ordinal);
- }
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index b4626b93fa..e8c636e7fb 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -12,6 +12,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
+using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -21,7 +22,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -454,7 +454,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
encodingParam = " -sub_charenc " + encodingParam;
}
- var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, EncodingUtils.NormalizePath(inputPath), EncodingUtils.NormalizePath(outputPath));
+ var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath.EscapeProcessArgument(), outputPath.EscapeProcessArgument());
await ExtractSubtitlesForFile(
inputPath,
@@ -632,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
streamIndex,
outputCodec,
outputFormatOption,
- EncodingUtils.NormalizePath(outputPath));
+ outputPath.EscapeProcessArgument());
}
await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
@@ -690,7 +690,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
streamIndex,
outputCodec,
outputFormatOption,
- EncodingUtils.NormalizePath(outputPath));
+ outputPath.EscapeProcessArgument());
}
if (outputPaths.Count > 0)
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 906efbcbcc..38f1cf738f 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
using ICU4N.Text;
@@ -173,5 +174,41 @@ namespace Jellyfin.Extensions
return cleaned;
}
+
+ /// <summary>
+ /// Escapes an argument so that it survives command line parsing as a single argument when it is wrapped in double quotes by the caller.
+ /// </summary>
+ /// <param name="value">The argument to escape.</param>
+ /// <returns>The escaped argument.</returns>
+ public static string EscapeProcessArgument(this string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var span = value.AsSpan();
+ if (!span.Contains('"'))
+ {
+ var trailing = span.Length - span.TrimEnd('\\').Length;
+ return trailing == 0 ? value : string.Concat(value, new string('\\', trailing));
+ }
+
+ var escaped = new StringBuilder(value.Length + 8);
+ var backslashes = 0;
+
+ foreach (var character in span)
+ {
+ if (character == '\\')
+ {
+ backslashes++;
+ continue;
+ }
+
+ escaped
+ .Append('\\', character == '"' ? (backslashes * 2) + 1 : backslashes)
+ .Append(character);
+ backslashes = 0;
+ }
+
+ return escaped.Append('\\', backslashes * 2).ToString();
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index 19c4514766..633c4f95ed 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -188,8 +188,8 @@ namespace Jellyfin.LiveTv.IO
var commandLineArgs = string.Format(
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
- inputTempFile,
- targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
+ inputTempFile.EscapeProcessArgument(),
+ targetFile.EscapeProcessArgument(),
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index 028f12afa7..0851570396 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -75,5 +75,28 @@ namespace Jellyfin.Extensions.Tests
var result = str.AsSpan().RightPart(needle).ToString();
Assert.Equal(expectedResult, result);
}
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("/media/movies/Film.mkv", "/media/movies/Film.mkv")]
+ [InlineData(@"C:\media\movies\Film.mkv", @"C:\media\movies\Film.mkv")]
+ [InlineData(@"/media/a""b.mkv", @"/media/a\""b.mkv")]
+ [InlineData(@"/media/a\""b.mkv", @"/media/a\\\""b.mkv")]
+ [InlineData(@"/media/a\\""b.mkv", @"/media/a\\\\\""b.mkv")]
+ [InlineData(@"/media/a\b""c.mkv", @"/media/a\b\""c.mkv")]
+ [InlineData(@"/media/trailing\", @"/media/trailing\\")]
+ [InlineData(@"/media/evil\"" -f lavfi -i sine .mkv", @"/media/evil\\\"" -f lavfi -i sine .mkv")]
+ public void EscapeProcessArgument_ValidInput_Corrects(string input, string expectedResult)
+ {
+ Assert.Equal(expectedResult, input.EscapeProcessArgument());
+ }
+
+ [Theory]
+ [InlineData("/media/movies/Film with spaces.mkv")]
+ [InlineData(@"C:\media\movies\Film.mkv")]
+ public void EscapeProcessArgument_NothingToEscape_ReturnsSameInstance(string input)
+ {
+ Assert.Same(input, input.EscapeProcessArgument());
+ }
}
}