diff --git a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs index abc9c0d82..de1a9e4f8 100644 --- a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs +++ b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs @@ -5,7 +5,10 @@ // the PC (on the PATH or where its installer puts it), so nobody has to download a second FFmpeg they already have. using System.Collections.Concurrent; +using System.Runtime.Versioning; +using System.Security; using Filee.Core.Settings; +using Microsoft.Win32; namespace Filee.Engines.Infrastructure; @@ -219,6 +222,8 @@ private static IEnumerable SystemFolders(string id) if (id == "calibre") folders.AddRange(["/Applications/calibre.app", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "calibre.app"), "/opt/calibre"]); } + else if (id == "libreoffice") + folders.AddRange(RegisteredFolders(LibreOfficeInstallPathKey)); foreach (var root in (OperatingSystem.IsWindows() ? new[] { Environment.SpecialFolder.ProgramFiles, Environment.SpecialFolder.ProgramFilesX86 } : []) @@ -256,6 +261,37 @@ private static IEnumerable SystemFolders(string id) .Where(f => !IsWithin(f, DownloadRoot, comparison) && !IsWithin(f, AppContext.BaseDirectory, comparison)); } + /// Where LibreOffice's Windows installer records its program folder (the key's default value). + internal const string LibreOfficeInstallPathKey = @"SOFTWARE\LibreOffice\UNO\InstallPath"; + + /// + /// The folders recorded as the default value of , for this user and for all users, in + /// the 64-bit and 32-bit views. LibreOffice installed somewhere else than Program Files (another drive, a folder + /// of its own) is found this way without being on the PATH. + /// + [SupportedOSPlatform("windows")] + internal static List RegisteredFolders(string keyPath) + { + var found = new List(); + foreach (var hive in new[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine }) + { + foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) + { + try + { + using var root = RegistryKey.OpenBaseKey(hive, view); + using var key = root.OpenSubKey(keyPath); + if (key?.GetValue(null) is string folder && folder.Trim().Trim('"') is { Length: > 0 } trimmed) + found.Add(Environment.ExpandEnvironmentVariables(trimmed)); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException) + { + } + } + } + return found; + } + private static bool IsWithin(string path, string root, StringComparison comparison) => string.Equals(Path.TrimEndingDirectorySeparator(path), Path.TrimEndingDirectorySeparator(root), comparison) || path.StartsWith(Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar, comparison); diff --git a/tests/Filee.Engines.Tests/OwnCopyTests.cs b/tests/Filee.Engines.Tests/OwnCopyTests.cs index 64486f274..cac063e11 100644 --- a/tests/Filee.Engines.Tests/OwnCopyTests.cs +++ b/tests/Filee.Engines.Tests/OwnCopyTests.cs @@ -89,6 +89,30 @@ public void A_tool_on_the_path_is_found_when_filee_has_no_copy() Assert.Equal(Path.Combine(_folder, EngineEnvironment.ProgramName("ffmpeg.exe")), EngineEnvironment.SystemProgram("ffmpeg", "ffmpeg.exe")); } + [Fact] + [System.Runtime.Versioning.SupportedOSPlatform("windows")] // skips elsewhere + public void LibreOffice_is_found_in_the_folder_its_installer_recorded() + { + Assert.SkipUnless(OperatingSystem.IsWindows(), "The Windows registry."); + const string key = @"Software\FileeTests\LibreOffice\UNO\InstallPath"; + File.WriteAllBytes(Path.Combine(_folder, "soffice.exe"), [0x4D, 0x5A]); + try + { + using (var written = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(key)) + written.SetValue(null, $"\"{_folder}\""); + + var folders = EngineEnvironment.RegisteredFolders(key); + + Assert.Contains(_folder, folders); + Assert.Equal(_folder, EngineEnvironment.FolderWithPrograms("libreoffice", _folder)); + Assert.Empty(EngineEnvironment.RegisteredFolders(@"Software\FileeTests\Missing")); + } + finally + { + Microsoft.Win32.Registry.CurrentUser.DeleteSubKeyTree(@"Software\FileeTests", throwOnMissingSubKey: false); + } + } + [Theory] [InlineData("ffmpeg", "ffmpeg version 7.1.1-full_build-www.gyan.dev Copyright (c) 2000-2025", "7.1")] [InlineData("ffmpeg", "ffmpeg version n8.0 Copyright (c) 2000-2025", "8.0")]