Skip to content
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
![Language](https://img.shields.io/badge/language-C%23-purple)
![License](https://img.shields.io/badge/license-Apache%202.0-green)

[Website](https://novasystemslab.org) ·
[Organization](https://novasystemslab.org) ·
[Discussions](https://github.com/Nova-Systems-Lab/WinDroid-Runtime/discussions) ·
[Issues](https://github.com/Nova-Systems-Lab/WinDroid-Runtime/issues)

**WinDroid Runtime** is an independent Android-compatible runtime and toolkit for Windows.

The long-term goal of this project is to build a modern platform that allows Android applications to run, integrate, and feel natural on Windows. The project is being designed from scratch, starting with developer tooling, APK management, ADB integration, and runtime research before moving toward experimental Android runtime and desktop integration research.
Expand Down Expand Up @@ -347,6 +352,8 @@ For official project work:
- Use GitHub Issues for confirmed bugs and actionable tasks.
- Use Pull Requests for code and documentation changes.

For private security reports, follow the [security policy](SECURITY.md) or contact [security@novasystemslab.org](mailto:security@novasystemslab.org).

## Current First Milestone

The first practical milestone is to build WinDroid Studio v0.1, a native Windows application that can:
Expand Down
71 changes: 71 additions & 0 deletions src/WinDroid.Adb/Models/AdbPathResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace WinDroid.Adb.Models;

/// <summary>
/// Represents the outcome of attempting to locate an ADB executable.
/// </summary>
/// <remarks>
/// A successful result carries a non-empty <see cref="Path"/> and a
/// <see langword="null"/> <see cref="ErrorMessage"/>. A failed result carries a
/// <see langword="null"/> <see cref="Path"/> and a non-empty
/// <see cref="ErrorMessage"/>. Use <see cref="Success"/> and <see cref="Failure"/>
/// to construct values that honour these invariants.
/// </remarks>
public sealed class AdbPathResult
{
/// <summary>
/// Gets a value indicating whether an ADB executable was located.
/// </summary>
public bool Found { get; init; }

/// <summary>
/// Gets the resolved absolute path to the ADB executable when
/// <see cref="Found"/> is <see langword="true"/>; otherwise
/// <see langword="null"/>.
/// </summary>
public string? Path { get; init; }

/// <summary>
/// Gets a concise, user-safe explanation of why resolution failed when
/// <see cref="Found"/> is <see langword="false"/>; otherwise
/// <see langword="null"/>.
/// </summary>
public string? ErrorMessage { get; init; }

/// <summary>
/// Creates a successful result for the given resolved executable path.
/// </summary>
/// <param name="path">The resolved absolute path to the ADB executable.</param>
/// <exception cref="ArgumentException">
/// <paramref name="path"/> is <see langword="null"/>, empty, or whitespace.
/// </exception>
public static AdbPathResult Success(string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);

return new AdbPathResult
{
Found = true,
Path = path,
ErrorMessage = null,
};
}

/// <summary>
/// Creates a failed result with the given user-safe error message.
/// </summary>
/// <param name="errorMessage">A concise explanation of the failure.</param>
/// <exception cref="ArgumentException">
/// <paramref name="errorMessage"/> is <see langword="null"/>, empty, or whitespace.
/// </exception>
public static AdbPathResult Failure(string errorMessage)
{
ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage);

return new AdbPathResult
{
Found = false,
Path = null,
ErrorMessage = errorMessage,
};
}
}
173 changes: 173 additions & 0 deletions src/WinDroid.Adb/Services/AdbPathResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using WinDroid.Adb.Models;

namespace WinDroid.Adb.Services;

/// <summary>
/// Locates an ADB executable by checking an optional custom path and then the
/// directories listed in the system <c>PATH</c> environment variable.
/// </summary>
/// <remarks>
/// 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 <c>PATH</c> is read on every call so that environment
/// changes during the application lifetime are honoured.
/// </remarks>
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"];

/// <summary>
/// Attempts to locate an ADB executable.
/// </summary>
/// <param name="customAdbPath">
/// Optional path to an ADB executable, typically supplied from
/// <c>AdbSettings.CustomAdbPath</c>. <see langword="null"/>, empty, or
/// whitespace values are ignored and resolution falls back to the system
/// <c>PATH</c>.
/// </param>
/// <returns>
/// A successful <see cref="AdbPathResult"/> containing the resolved path, or
/// a failed result whose <see cref="AdbPathResult.ErrorMessage"/> explains
/// why ADB could not be found. A missing ADB is an expected outcome and is
/// reported through the result rather than by throwing.
/// </returns>
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));
}

/// <summary>
/// Searches the directories listed in the system <c>PATH</c> for a known ADB
/// executable name, returning the first match as an absolute path.
/// </summary>
private static string? SearchSystemPath()
{
string? pathVariable = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(pathVariable))
{
return null;
}

var visitedDirectories = new HashSet<string>(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;
}

/// <summary>
/// Returns the absolute path to the first known ADB executable found in the
/// given directory, or <see langword="null"/> if none is present or the
/// entry cannot be inspected.
/// </summary>
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;
}

/// <summary>
/// Trims surrounding whitespace and a single pair of matching surrounding
/// quotation marks, returning <see langword="null"/> when nothing usable
/// remains.
/// </summary>
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;
}

/// <summary>
/// Produces an absolute, normalized path, returning <see langword="null"/>
/// if the path cannot be normalized.
/// </summary>
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.";
}
Loading