Normalize fix, apply in more places

This commit is contained in:
Cody Robibero
2026-08-20 18:10:50 -04:00
parent a8da0664a3
commit 9ba91d4583
9 changed files with 76 additions and 29 deletions
@@ -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);
@@ -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>
@@ -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.
@@ -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;
@@ -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);
}
}
}
@@ -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)
@@ -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();
}
}
}
+2 -2
View File
@@ -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,
@@ -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());
}
}
}