diff --git a/src/WinDroid.Adb/Models/AdbPathResult.cs b/src/WinDroid.Adb/Models/AdbPathResult.cs new file mode 100644 index 0000000..f20cfe7 --- /dev/null +++ b/src/WinDroid.Adb/Models/AdbPathResult.cs @@ -0,0 +1,71 @@ +namespace WinDroid.Adb.Models; + +/// +/// Represents the outcome of attempting to locate an ADB executable. +/// +/// +/// A successful result carries a non-empty and a +/// . A failed result carries a +/// and a non-empty +/// . Use and +/// to construct values that honour these invariants. +/// +public sealed class AdbPathResult +{ + /// + /// Gets a value indicating whether an ADB executable was located. + /// + public bool Found { get; init; } + + /// + /// Gets the resolved absolute path to the ADB executable when + /// is ; otherwise + /// . + /// + public string? Path { get; init; } + + /// + /// Gets a concise, user-safe explanation of why resolution failed when + /// is ; otherwise + /// . + /// + public string? ErrorMessage { get; init; } + + /// + /// Creates a successful result for the given resolved executable path. + /// + /// The resolved absolute path to the ADB executable. + /// + /// is , empty, or whitespace. + /// + public static AdbPathResult Success(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + return new AdbPathResult + { + Found = true, + Path = path, + ErrorMessage = null, + }; + } + + /// + /// Creates a failed result with the given user-safe error message. + /// + /// A concise explanation of the failure. + /// + /// is , empty, or whitespace. + /// + public static AdbPathResult Failure(string errorMessage) + { + ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage); + + return new AdbPathResult + { + Found = false, + Path = null, + ErrorMessage = errorMessage, + }; + } +} diff --git a/src/WinDroid.Adb/Services/AdbPathResolver.cs b/src/WinDroid.Adb/Services/AdbPathResolver.cs new file mode 100644 index 0000000..87cfa96 --- /dev/null +++ b/src/WinDroid.Adb/Services/AdbPathResolver.cs @@ -0,0 +1,173 @@ +using WinDroid.Adb.Models; + +namespace WinDroid.Adb.Services; + +/// +/// Locates an ADB executable by checking an optional custom path and then the +/// directories listed in the system PATH environment variable. +/// +/// +/// This type only determines whether an ADB executable exists on disk. It does +/// not launch ADB and does not verify that a located file is a genuine ADB +/// binary. The system PATH is read on every call so that environment +/// changes during the application lifetime are honoured. +/// +public sealed class AdbPathResolver +{ + // The active application target is Windows, where the executable is named + // "adb.exe". The plain "adb" name is also checked so the portable library + // behaves sensibly on non-Windows hosts. "adb.exe" is checked first so + // resolution is deterministic when both names exist in the same directory. + private static readonly string[] ExecutableNames = ["adb.exe", "adb"]; + + /// + /// Attempts to locate an ADB executable. + /// + /// + /// Optional path to an ADB executable, typically supplied from + /// AdbSettings.CustomAdbPath. , empty, or + /// whitespace values are ignored and resolution falls back to the system + /// PATH. + /// + /// + /// A successful containing the resolved path, or + /// a failed result whose explains + /// why ADB could not be found. A missing ADB is an expected outcome and is + /// reported through the result rather than by throwing. + /// + public AdbPathResult Resolve(string? customAdbPath = null) + { + string? normalizedCustomPath = NormalizeToken(customAdbPath); + bool customPathSupplied = normalizedCustomPath is not null; + + if (customPathSupplied && File.Exists(normalizedCustomPath)) + { + string? resolved = GetFullPathSafe(normalizedCustomPath!); + if (resolved is not null) + { + return AdbPathResult.Success(resolved); + } + + // The file exists but its path could not be normalized to an + // absolute path, so fall through to the system PATH search. + } + + string? pathMatch = SearchSystemPath(); + if (pathMatch is not null) + { + return AdbPathResult.Success(pathMatch); + } + + return AdbPathResult.Failure(BuildNotFoundMessage(customPathSupplied)); + } + + /// + /// Searches the directories listed in the system PATH for a known ADB + /// executable name, returning the first match as an absolute path. + /// + private static string? SearchSystemPath() + { + string? pathVariable = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(pathVariable)) + { + return null; + } + + var visitedDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string rawEntry in pathVariable.Split(Path.PathSeparator)) + { + string? directory = NormalizeToken(rawEntry); + if (directory is null || !visitedDirectories.Add(directory)) + { + // Blank or duplicate entry; skip without touching the filesystem. + continue; + } + + string? match = FindExecutableInDirectory(directory); + if (match is not null) + { + return match; + } + } + + return null; + } + + /// + /// Returns the absolute path to the first known ADB executable found in the + /// given directory, or if none is present or the + /// entry cannot be inspected. + /// + private static string? FindExecutableInDirectory(string directory) + { + foreach (string executableName in ExecutableNames) + { + string candidate = Path.Combine(directory, executableName); + + // File.Exists is exception-free and returns false for malformed or + // inaccessible paths, so unusable PATH entries are skipped safely. + if (File.Exists(candidate)) + { + string? resolved = GetFullPathSafe(candidate); + if (resolved is not null) + { + return resolved; + } + + // Exists but cannot be normalized to an absolute path; skip it. + } + } + + return null; + } + + /// + /// Trims surrounding whitespace and a single pair of matching surrounding + /// quotation marks, returning when nothing usable + /// remains. + /// + private static string? NormalizeToken(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + string trimmed = value.Trim(); + + if (trimmed.Length >= 2 && trimmed[0] == '"' && trimmed[^1] == '"') + { + trimmed = trimmed[1..^1].Trim(); + } + + return string.IsNullOrEmpty(trimmed) ? null : trimmed; + } + + /// + /// Produces an absolute, normalized path, returning + /// if the path cannot be normalized. + /// + private static string? GetFullPathSafe(string path) + { + try + { + return Path.GetFullPath(path); + } + catch (Exception ex) when ( + ex is ArgumentException + or PathTooLongException + or NotSupportedException + or System.Security.SecurityException) + { + return null; + } + } + + private static string BuildNotFoundMessage(bool customPathSupplied) => + customPathSupplied + ? "ADB could not be found. The supplied custom path did not point to a " + + "valid ADB executable file, and no ADB executable was found through " + + "the system PATH." + : "ADB could not be found through the system PATH."; +}