Safeguard against invalid GUIDs (#16813)

Safeguard against invalid GUIDs
This commit is contained in:
Tim Eisele 2026-05-12 18:12:54 +02:00 committed by GitHub
parent c169184e01
commit a9865367d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 139 additions and 42 deletions

View file

@ -6,6 +6,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@ -14,18 +15,22 @@ namespace Emby.Server.Implementations.Library;
/// </summary>
public class PathManager : IPathManager
{
private readonly ILogger<PathManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
ILogger<PathManager> logger,
IServerConfigurationManager config,
IApplicationPaths appPaths)
{
_logger = logger;
_config = config;
_appPaths = appPaths;
}
@ -35,31 +40,43 @@ public class PathManager : IPathManager
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
public string GetAttachmentPath(string mediaSourceId, string fileName)
public string? GetAttachmentPath(string mediaSourceId, string fileName)
{
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
var folder = GetAttachmentFolderPath(mediaSourceId);
return folder is null ? null : Path.Combine(folder, fileName);
}
/// <inheritdoc />
public string GetAttachmentFolderPath(string mediaSourceId)
public string? GetAttachmentFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
if (!Guid.TryParse(mediaSourceId, out var parsed))
{
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk attachment folder.", mediaSourceId);
return null;
}
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(AttachmentCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitleFolderPath(string mediaSourceId)
public string? GetSubtitleFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
if (!Guid.TryParse(mediaSourceId, out var parsed))
{
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk subtitle folder.", mediaSourceId);
return null;
}
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(SubtitleCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
var folder = GetSubtitleFolderPath(mediaSourceId);
return folder is null ? null : Path.Combine(folder, streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
@ -90,12 +107,23 @@ public class PathManager : IPathManager
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
return [
GetAttachmentFolderPath(mediaSourceId),
GetSubtitleFolderPath(mediaSourceId),
GetTrickplayDirectory(item, false),
GetTrickplayDirectory(item, true),
GetChapterImageFolderPath(item)
];
List<string> paths = [];
var attachmentFolder = GetAttachmentFolderPath(mediaSourceId);
if (attachmentFolder is not null)
{
paths.Add(attachmentFolder);
}
var subtitleFolder = GetSubtitleFolderPath(mediaSourceId);
if (subtitleFolder is not null)
{
paths.Add(subtitleFolder);
}
paths.Add(GetTrickplayDirectory(item, false));
paths.Add(GetTrickplayDirectory(item, true));
paths.Add(GetChapterImageFolderPath(item));
return paths;
}
}

View file

@ -7,6 +7,7 @@ using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -283,9 +284,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
var deleted = DeleteItems(itemsToDelete!);
_logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", itemsToDelete.Count);
_logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", deleted);
}
private void CleanupOrphanedAlternateVersionBaseItems(JellyfinDbContext context)
@ -314,9 +315,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
var deleted = DeleteItems(itemsToDelete!);
_logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", itemsToDelete.Count);
_logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", deleted);
}
private void CleanupItemsFromDeletedLibraries(JellyfinDbContext context)
@ -343,9 +344,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
var deleted = DeleteItems(itemsToDelete!);
_logger.LogInformation("Removed {Count} items from deleted libraries.", itemsToDelete.Count);
_logger.LogInformation("Removed {Count} items from deleted libraries.", deleted);
}
private void CleanupStaleFileEntries(JellyfinDbContext context)
@ -431,9 +432,34 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
var deleted = DeleteItems(itemsToDelete!);
_logger.LogInformation("Removed {Count} stale items.", itemsToDelete.Count);
_logger.LogInformation("Removed {Count} stale items.", deleted);
}
private int DeleteItems(IReadOnlyCollection<BaseItem> items)
{
if (items.Count == 0)
{
return 0;
}
var options = new DeleteOptions { DeleteFileLocation = false, DeleteFromExternalProvider = false };
var deleted = 0;
foreach (var item in items)
{
try
{
_libraryManager.DeleteItem(item, options);
deleted++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Skipping item {ItemId} ({ItemName}): delete failed.", item.Id, item.Name ?? "Unknown");
}
}
return deleted;
}
private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)

View file

@ -144,6 +144,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
}
var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension);
if (newSubtitleCachePath is null)
{
continue;
}
if (File.Exists(newSubtitleCachePath))
{
File.Delete(oldSubtitleCachePath);
@ -182,6 +187,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
}
var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.Filename ?? attachmentIndex.ToString(CultureInfo.InvariantCulture));
if (newAttachmentPath is null)
{
continue;
}
if (File.Exists(newAttachmentPath))
{
File.Delete(oldAttachmentPath);

View file

@ -22,30 +22,30 @@ public interface IPathManager
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="streamIndex">The stream index.</param>
/// <param name="extension">The subtitle file extension.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitleFolderPath(string mediaSourceId);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetSubtitleFolderPath(string mediaSourceId);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="fileName">The attachmentFileName index.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentPath(string mediaSourceId, string fileName);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetAttachmentPath(string mediaSourceId, string fileName);
/// <summary>
/// Gets the path to the attachment folder.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentFolderPath(string mediaSourceId);
/// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
public string? GetAttachmentFolderPath(string mediaSourceId);
/// <summary>
/// Gets the chapter images data path.

View file

@ -1880,7 +1880,9 @@ namespace MediaBrowser.Controller.MediaEncoding
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
var fontParam = string.Format(
var fontParam = fontPath is null
? string.Empty
: string.Format(
CultureInfo.InvariantCulture,
":fontsdir='{0}'",
_mediaEncoder.EscapeSubtitleFilterPath(fontPath));

View file

@ -129,6 +129,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
ArgumentException.ThrowIfNullOrEmpty(inputPath);
var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
if (outputFolder is null)
{
_logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile);
return;
}
using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
var directory = Directory.CreateDirectory(outputFolder);
@ -241,9 +247,14 @@ namespace MediaBrowser.MediaEncoding.Attachments
CancellationToken cancellationToken)
{
var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
if (attachmentFolderPath is null)
{
throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no attachment cache (non-GUID Id, e.g. Live TV stream).");
}
using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
{
var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture))!;
if (!File.Exists(attachmentPath))
{
await ExtractAttachmentInternal(

View file

@ -212,7 +212,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream);
var outputFormat = GetExtractableSubtitleFormat(subtitleStream);
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension);
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension)
?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream).");
return new SubtitleInfo()
{
@ -242,7 +243,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!_subtitleParser.SupportsFileExtension(currentFormat))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt")
?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream).");
await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
@ -520,6 +522,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
if (outputPath is null)
{
continue;
}
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
@ -591,6 +597,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
if (outputPath is null)
{
continue;
}
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
@ -636,6 +647,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
if (outputPath is null)
{
continue;
}
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
@ -968,7 +984,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
{
return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
}
@ -981,10 +997,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
if (cachePath is not null)
{
path = cachePath;
await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
.ConfigureAwait(false);
}
}
var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
var charset = result.Detected?.EncodingName ?? string.Empty;