Skip to content

Commit

Permalink
Add rules
Browse files Browse the repository at this point in the history
  • Loading branch information
dorssel committed Apr 13, 2024
1 parent 46c7f5c commit 7c3ef3c
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 29 deletions.
4 changes: 4 additions & 0 deletions Installer/Server.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ SPDX-License-Identifier: GPL-3.0-only
Key="Devices"
ForceCreateOnInstall="yes"
/>
<RegistryKey
Key="Rules"
ForceCreateOnInstall="yes"
/>
</RegistryKey>
<Environment
Id="PATH"
Expand Down
28 changes: 14 additions & 14 deletions UnitTests/BusId_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ sealed class BusIdData
"0-0",
"0-1",
"1-0",
"1-65536",
"65536-1",
"1-100",
"100-1",
"01-1",
"1-01",
];
Expand All @@ -89,20 +89,20 @@ public static IEnumerable<string[]> Invalid
"IncompatibleHub",
"1-1",
"1-2",
"1-65534",
"1-65535",
"1-98",
"1-99",
"2-1",
"2-2",
"2-65534",
"2-65535",
"65534-1",
"65534-2",
"65534-65534",
"65534-65535",
"65535-1",
"65535-2",
"65535-65534",
"65535-65535",
"2-98",
"2-99",
"98-1",
"98-2",
"98-98",
"98-99",
"99-1",
"99-2",
"99-98",
"99-99",
];

public static IEnumerable<string[]> Valid => from value in _Valid select new string[] { value };
Expand Down
4 changes: 2 additions & 2 deletions UnitTests/Parse_usbipd_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ sealed class Parse_usbipd_Tests
: ParseTestBase
{
[TestMethod]
public void Success()
public void NoCommand()
{
Test(ExitCode.Success);
Test(ExitCode.ParseError);
}

[TestMethod]
Expand Down
10 changes: 6 additions & 4 deletions Usbipd.Automation/BusId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ public BusId(ushort bus, ushort port)
{
// Do not allow the explicit creation of the special IncompatibleHub value.
// Instead, use the static IncompatibleHub field (preferrable) or "default".
if (bus == 0)
// USB supports up to 127 devices, but that would require multiple hubs; the "per hub" port will never be >99.
// And if you have more than 99 hubs on one system, then you win a prize! (but we're not going to support it...)
if (bus == 0 || bus > 99)
{
throw new ArgumentOutOfRangeException(nameof(bus));
}
if (port == 0)
if (port == 0 || port > 99)
{
throw new ArgumentOutOfRangeException(nameof(port));
}
Expand Down Expand Up @@ -63,8 +65,8 @@ public static bool TryParse(string input, out BusId busId)
}
var match = Regex.Match(input, "^([1-9][0-9]*)-([1-9][0-9]*)$");
if (match.Success
&& ushort.TryParse(match.Groups[1].Value, out var bus)
&& ushort.TryParse(match.Groups[2].Value, out var port))
&& ushort.TryParse(match.Groups[1].Value, out var bus) && bus <= 99
&& ushort.TryParse(match.Groups[2].Value, out var port) && port <= 99)
{
busId = new(bus, port);
return true;
Expand Down
60 changes: 59 additions & 1 deletion Usbipd/CommandHandlersCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ Task<ExitCode> ICommandHandlers.List(bool usbids, IConsole console, Cancellation
{
state = "Not shared";
}
// NOTE: Strictly speaking, both Bus and Port can be > 99. If you have one of those, you win a prize!
console.Write($"{(device.BusId.Value.IsIncompatibleHub ? string.Empty : device.BusId.Value),-5} ");
console.Write($"{device.HardwareId,-9} ");
console.WriteTruncated(GetDescription(device, usbids), 60, true);
Expand Down Expand Up @@ -474,4 +473,63 @@ Task<ExitCode> ICommandHandlers.State(IConsole console, CancellationToken cancel
Console.Write(json);
return Task.FromResult(ExitCode.Success);
}

Task<ExitCode> ICommandHandlers.RuleAdd(Rule rule, IConsole console, CancellationToken cancellationToken)
{
if (RegistryUtils.GetRules().FirstOrDefault(r => r.Value == rule) is var existingRule && existingRule.Key != default)
{
console.ReportError($"Rule already exists with guid '{existingRule.Key:D}'.");
return Task.FromResult(ExitCode.Failure);

Check warning on line 482 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L481-L482

Added lines #L481 - L482 were not covered by tests
}

if (!CheckWriteAccess(console))
{
return Task.FromResult(ExitCode.AccessDenied);

Check warning on line 487 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L487

Added line #L487 was not covered by tests
}

var guid = RegistryUtils.AddRule(rule);
console.ReportInfo($"Rule created with guid '{guid:D}'.");
return Task.FromResult(ExitCode.Success);

Check warning on line 492 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L490-L492

Added lines #L490 - L492 were not covered by tests
}

Task<ExitCode> ICommandHandlers.RuleList(IConsole console, CancellationToken cancellationToken)
{
var allRules = RegistryUtils.GetRules();
console.WriteLine("Rules:");
console.WriteLine($"{"GUID",-36} {"TYPE",-4} {"ACCESS",-6} {"BUSID",-5} {"VID:PID",-9}");

Check warning on line 499 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L497-L499

Added lines #L497 - L499 were not covered by tests
foreach (var rule in allRules)
{
console.Write($"{rule.Key,-36} ");
console.Write($"{rule.Value.Type,-4} ");

Check warning on line 503 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L502-L503

Added lines #L502 - L503 were not covered by tests
console.Write($"{(rule.Value.Allow ? "Allow" : "Deny"),-6} ");
switch (rule.Value.Type)
{
case RuleType.Bind:
var ruleBind = (RuleBind)rule.Value;

Check warning on line 508 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L508

Added line #L508 was not covered by tests
console.Write($"{(ruleBind.BusId.HasValue ? ruleBind.BusId.Value : string.Empty),-5} ");
console.Write($"{(ruleBind.HardwareId.HasValue ? ruleBind.HardwareId.Value : string.Empty),-9}");
break;
}
console.WriteLine(string.Empty);

Check warning on line 513 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L513

Added line #L513 was not covered by tests
}
console.WriteLine(string.Empty);
return Task.FromResult(ExitCode.Success);

Check warning on line 516 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L515-L516

Added lines #L515 - L516 were not covered by tests
}

Task<ExitCode> ICommandHandlers.RuleRemove(Guid guid, IConsole console, CancellationToken cancellationToken)
{
if (!RegistryUtils.GetRules().ContainsKey(guid))
{
console.ReportError($"There is no rule with guid '{guid:D}'.");
return Task.FromResult(ExitCode.Failure);

Check warning on line 524 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L523-L524

Added lines #L523 - L524 were not covered by tests
}

if (!CheckWriteAccess(console))
{
return Task.FromResult(ExitCode.AccessDenied);

Check warning on line 529 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L529

Added line #L529 was not covered by tests
}

RegistryUtils.RemoveRule(guid);
return Task.FromResult(ExitCode.Success);

Check warning on line 533 in Usbipd/CommandHandlersCli.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/CommandHandlersCli.cs#L532-L533

Added lines #L532 - L533 were not covered by tests
}
}
3 changes: 3 additions & 0 deletions Usbipd/ICommandHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ interface ICommandHandlers
public Task<ExitCode> State(IConsole console, CancellationToken cancellationToken);
public Task<ExitCode> Install(IConsole console, CancellationToken cancellationToken);
public Task<ExitCode> Uninstall(IConsole console, CancellationToken cancellationToken);
public Task<ExitCode> RuleAdd(Rule rule, IConsole console, CancellationToken cancellationToken);
public Task<ExitCode> RuleList(IConsole console, CancellationToken cancellationToken);
public Task<ExitCode> RuleRemove(Guid guid, IConsole console, CancellationToken cancellationToken);
}
14 changes: 14 additions & 0 deletions Usbipd/ParseResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
//
// SPDX-License-Identifier: GPL-3.0-only

using System.CommandLine;
using System.CommandLine.Parsing;

namespace Usbipd;

static class ParseResultExtensions
{
public static T? GetValueForOptionOrNull<T>(this ParseResult parseResult, Option<T> option) where T : struct
=> parseResult.HasOption(option) ? parseResult.GetValueForOption(option) : null;
}
161 changes: 153 additions & 8 deletions Usbipd/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.CommandLine.Builder;
using System.CommandLine.Completions;
using System.CommandLine.Help;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Reflection;
Expand Down Expand Up @@ -63,6 +62,17 @@ static string OneOfRequiredText(params Option[] options)
return $"Exactly one of the options {list} is required.";
}

static string AtLeastOneOfRequiredText(params Option[] options)
{
Debug.Assert(options.Length >= 2);

var names = options.Select(o => $"'--{o.Name}'").ToArray();
var list = names.Length == 2
? $"{names[0]} or {names[1]}"
: string.Join(", ", names[0..(names.Length - 1)]) + ", or " + names[^1];
return $"At least one of the options {list} is required.";
}

static void ValidateOneOf(CommandResult commandResult, params Option[] options)
{
Debug.Assert(options.Length >= 2);
Expand All @@ -73,6 +83,16 @@ static void ValidateOneOf(CommandResult commandResult, params Option[] options)
}
}

static void ValidateAtLeastOneOf(CommandResult commandResult, params Option[] options)
{
Debug.Assert(options.Length >= 2);

if (!options.Any(option => commandResult.FindResultFor(option) is not null))
{
commandResult.ErrorMessage = AtLeastOneOfRequiredText(options);

Check warning on line 92 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L92

Added line #L92 was not covered by tests
}
}

Check warning on line 94 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L94

Added line #L94 was not covered by tests

internal static IEnumerable<string> CompletionGuard(CompletionContext completionContext, Func<IEnumerable<string>?> complete)
{
try
Expand Down Expand Up @@ -115,11 +135,6 @@ internal static int Main(params string[] args)
internal static ExitCode Run(IConsole? optionalTestConsole, ICommandHandlers commandHandlers, params string[] args)
{
var rootCommand = new RootCommand("Shares locally connected USB devices to other machines, including Hyper-V guests and WSL 2.");
rootCommand.SetHandler(invocationContext =>
{
invocationContext.HelpBuilder.Write(rootCommand, invocationContext.Console.Out.CreateTextWriter());
});

{
//
// attach [--auto-attach]
Expand Down Expand Up @@ -169,7 +184,7 @@ internal static ExitCode Run(IConsole? optionalTestConsole, ICommandHandlers com
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
UsbDevice.GetAll().Where(d => d.BusId.HasValue).GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
//
// wsl attach
// attach
//
var attachCommand = new Command("attach", "Attach a USB device to a client\0"
+ "Attaches a USB device to a client.\n"
Expand Down Expand Up @@ -317,7 +332,7 @@ await commandHandlers.Bind(invocationContext.ParseResult.GetValueForOption(hardw
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
UsbDevice.GetAll().Where(d => d.BusId.HasValue).GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
//
// wsl detach
// detach
//
var detachCommand = new Command("detach", "Detach a USB device from a client\0"
+ "Detaches one or more USB devices. The client sees this as a surprise "
Expand Down Expand Up @@ -396,6 +411,136 @@ await commandHandlers.List(invocationContext.ParseResult.HasOption(usbidsOption)
});
rootCommand.AddCommand(listCommand);
}
{
//
// rule
//
var ruleCommand = new Command("rule", "Manage rules\0"
+ "Rules allow or deny specific functionality, such as bind and attach.\n");
{
//
// rule add --access <ACCESS>
//
var accessOption = new Option<RuleAccess>(
aliases: ["--access", "-a"]
)
{
ArgumentHelpName = "ACCESS",
Description = "Allow or Deny",
IsRequired = true,
};
//
// rule add --type <TYPE>
//
var typeOption = new Option<RuleType>(
aliases: ["--type", "-t"]
)
{
ArgumentHelpName = "TYPE",
Description = "Add a rule of type <TYPE>",
IsRequired = true,
};
//
// rule add [--busid <BUSID>]
//
var busIdOption = new Option<BusId>(
aliases: ["--busid", "-b"],
parseArgument: ParseCompatibleBusId
)
{
ArgumentHelpName = "BUSID",
Description = "Share device having <BUSID>",
}.AddCompletions(CompatibleBusIdCompletions);
//
// rule add [--hardware-id <VID>:<PID>]
//
var hardwareIdOption = new Option<VidPid>(
// NOTE: the alias '-h' is already for '--help'
aliases: ["--hardware-id", "-i"],
parseArgument: ParseVidPid
)
{
ArgumentHelpName = "VID:PID",
Description = "Attach device having <VID>:<PID>",
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
UsbDevice.GetAll().GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
//
// rule add
//
var addCommand = new Command("add", "Add a rule\0"
+ "Add a new rule. The resulting rule set will be effective immediately.\n"
+ "\n"
+ AtLeastOneOfRequiredText(busIdOption, hardwareIdOption))
{
accessOption,
typeOption,
busIdOption,
hardwareIdOption,
};
addCommand.AddValidator(commandResult =>
{
ValidateAtLeastOneOf(commandResult, busIdOption, hardwareIdOption);

Check warning on line 482 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L482

Added line #L482 was not covered by tests
});
addCommand.SetHandler(async invocationContext =>
{
var ruleType = invocationContext.ParseResult.GetValueForOption(typeOption);
invocationContext.ExitCode = (int)await (ruleType switch
{
RuleType.Bind =>
commandHandlers.RuleAdd(new RuleBind(invocationContext.ParseResult.GetValueForOption(accessOption) == RuleAccess.Allow,
invocationContext.ParseResult.GetValueForOptionOrNull(busIdOption),
invocationContext.ParseResult.GetValueForOptionOrNull(hardwareIdOption)),
invocationContext.Console, invocationContext.GetCancellationToken()),
_ => throw new UnexpectedResultException($"Unexpected rule type '{ruleType}'."),
});

Check warning on line 495 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L486-L495

Added lines #L486 - L495 were not covered by tests
});
ruleCommand.AddCommand(addCommand);
}
{
//
// rule list
//
var listCommand = new Command("list", "List rules\0"
+ "List all rules.\n");
listCommand.SetHandler(async invocationContext =>
{
invocationContext.ExitCode = (int)
await commandHandlers.RuleList(invocationContext.Console, invocationContext.GetCancellationToken());

Check warning on line 508 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L507-L508

Added lines #L507 - L508 were not covered by tests
});
ruleCommand.AddCommand(listCommand);
}
{
//
// rule remove --guid <GUID>
//
var guidOption = new Option<Guid>(
aliases: ["--guid", "-g"],
parseArgument: ParseGuid
)
{
ArgumentHelpName = "GUID",
Description = "Stop sharing persisted device having <GUID>",
IsRequired = true,
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
RegistryUtils.GetRules().Select(r => r.Key.ToString("D"))));
//
// rule remove
//
var removeCommand = new Command("remove", "Remove a rule\0"
+ "Remove an existing rule. The resulting rule set will be effective immediately.\n")
{
guidOption,
};
removeCommand.SetHandler(async invocationContext =>
{
invocationContext.ExitCode = (int)
await commandHandlers.RuleRemove(invocationContext.ParseResult.GetValueForOption(guidOption),
invocationContext.Console, invocationContext.GetCancellationToken());

Check warning on line 538 in Usbipd/Program.cs

View check run for this annotation

Codecov / codecov/patch

Usbipd/Program.cs#L536-L538

Added lines #L536 - L538 were not covered by tests
});
ruleCommand.AddCommand(removeCommand);
}
rootCommand.AddCommand(ruleCommand);
}
{
//
// server [<KEY=VALUE>...]
Expand Down
Loading

0 comments on commit 7c3ef3c

Please sign in to comment.