XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using XFESystem.Browser;
using XFESystem.Core;
using XFESystem.Desktop;
using XFESystem.NativeKernel;
using XFESystem.NativeRuntime.Application;
using XFESystem.Platform;

namespace XFESystem.Platform.Tests;

public sealed class Ps2InputTests
{
    [Fact]
    public void TerminalReceivesInsertAndControlCWithoutBrowserInterception()
    {
        var actions = Drain(new Ps2InputSource(new Reports(Keys(0xE0, 0x52, 0xE0, 0xD2, 0x1D, 0x2E, 0xAE, 0x9D)),
            1024, 768, 100, () => new(false, DesktopApplication.Terminal)));
        Assert.Collection(actions,
            item => Assert.Equal(DesktopKey.Insert, item.Key),
            item => Assert.Equal(DesktopKey.Cancel, item.Key));
    }

    [Fact]
    public void ReportAbiIs32BytesAndDecoderStateHasNoManagedReferences()
    {
        Assert.Equal(32, Marshal.SizeOf<NativeInputReport>());
        Assert.Equal(16, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.DeltaX)).ToInt32());
        Assert.Equal(24, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Buttons)).ToInt32());
        Assert.Equal(28, Marshal.OffsetOf<NativeInputReport>(nameof(NativeInputReport.Wheel)).ToInt32());
        Assert.False(RuntimeHelpers.IsReferenceOrContainsReferences<Ps2DecodeState>());
    }

    [Fact]
    public void ExtendedMakeBreakRetainsIdentityAndTwoControlKeysDoNotReleaseEachOther()
    {
        var reports = Keys(0x1D, 0xE0, 0x1D, 0x9D, 0xE0, 0x9D, 0xE0, 0x4B, 0xE0, 0xCB);
        Assert.Equal(6, reports.Length);
        Assert.Equal(0x11Du, reports[1].KeyCode); Assert.Equal(Ps2Decoder.Control, reports[2].Modifiers);
        Assert.Equal(0u, reports[3].Modifiers); Assert.Equal(0x14Bu, reports[4].KeyCode);
        Assert.Equal(Ps2Decoder.KeyUp, reports[5].Kind); Assert.Equal(0x14Bu, reports[5].KeyCode);
        Assert.Equal(0u, reports[5].Unicode);
    }

    [Fact]
    public void CompletePauseSequenceDoesNotToggleNumLockOrPressControl()
    {
        var reports = Keys(0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5, 0x1E, 0x9E);
        Assert.Equal(3, reports.Length); Assert.Equal(Ps2Decoder.PauseKey, reports[0].KeyCode);
        Assert.Equal(0u, reports[0].Modifiers); Assert.Equal((uint)'a', reports[1].Unicode);
    }

    [Fact]
    public void CorruptE1AndHardwareOverrunProduceRecoveryAndClearHeldModifiers()
    {
        var reports = Keys(0x2A, 0xE1, 0x1D, 0x00, 0x1E, 0xFF, 0x30);
        Assert.Equal(Ps2Decoder.Lost, reports[1].Kind); Assert.Equal(0u, reports[1].Modifiers);
        Assert.Equal((uint)'a', reports[2].Unicode); Assert.Equal(Ps2Decoder.Lost, reports[3].Kind);
        Assert.Equal((uint)'b', reports[4].Unicode);
    }

    [Fact]
    public void PrintScreenFakeShiftDoesNotCreateAStickyModifier()
    {
        var reports = Keys(0xE0, 0x2A, 0xE0, 0x37, 0xE0, 0xB7, 0xE0, 0xAA, 0x1E);
        Assert.Equal(3, reports.Length); Assert.Equal(0x137u, reports[0].KeyCode);
        Assert.Equal(Ps2Decoder.KeyUp, reports[1].Kind); Assert.All(reports, report => Assert.Equal(0u, report.Modifiers));
        Assert.Equal((uint)'a', reports[2].Unicode);
    }

    [Fact]
    public void CapsOnlyTogglesOnFirstMakeAndCombinesWithShiftUsingXor()
    {
        var reports = Keys(0x3A, 0x3A, 0x1E, 0x9E, 0x2A, 0x1E, 0x9E, 0xAA, 0x1E, 0x9E, 0xBA, 0x3A, 0x1E);
        Assert.Equal((uint)'A', reports[2].Unicode);
        Assert.Equal((uint)'a', reports[5].Unicode);
        Assert.Equal((uint)'A', reports[8].Unicode);
        Assert.Equal((uint)'a', reports[^1].Unicode);
    }

    [Fact]
    public void OverrunWhileCapsIsHeldDoesNotTurnItsNextTypematicMakeIntoAnotherToggle()
    {
        var reports = Keys(0x3A, 0x00, 0x3A, 0x1E, 0xBA, 0x3A, 0x1E);
        Assert.Equal(Ps2Decoder.Lost, reports[1].Kind);
        Assert.Equal(Ps2Decoder.CapsLock, reports[2].Modifiers);
        Assert.Equal((uint)'A', reports[3].Unicode); Assert.Equal((uint)'a', reports[^1].Unicode);
    }

    [Fact]
    public void NumLockKeypadCharactersRemainDistinctFromExtendedNavigation()
    {
        var actions = Actions(Keys(0x45, 0xC5, 0x4B, 0xCB, 0xE0, 0x4B, 0xE0, 0xCB));
        Assert.Collection(actions,
            item => { Assert.Equal(DesktopKey.Character, item.Key); Assert.Equal('4', item.Character); },
            item => Assert.Equal(DesktopKey.Left, item.Key));
    }

    [Fact]
    public void MouseResynchronizesHeaderAndDecodesNineBitSignedScreenDeltas()
    {
        var reports = Mouse(0x00, 0x07, 0x18, 0xFF, 0x02, 0x29, 0x01, 0xFF);
        Assert.Equal(2, reports.Length);
        Assert.Equal(-1, reports[0].DeltaX); Assert.Equal(-2, reports[0].DeltaY);
        Assert.Equal(1, reports[1].DeltaX); Assert.Equal(1, reports[1].DeltaY); Assert.Equal(1u, reports[1].Buttons);
    }

    [Fact]
    public void MouseOverflowKeepsRealButtonsButDiscardsMotionAndRecoversNextPacket()
    {
        var reports = Mouse(0xC9, 0x7F, 0x7F, 0x08, 0x02, 0x00);
        Assert.Equal(Ps2Decoder.Lost, reports[0].Kind); Assert.Equal(1u, reports[0].Buttons);
        Assert.Equal(0, reports[0].DeltaX); Assert.Equal(0, reports[0].DeltaY);
        Assert.Equal(Ps2Decoder.Pointer, reports[1].Kind); Assert.Equal(0u, reports[1].Buttons); Assert.Equal(2, reports[1].DeltaX);
    }

    [Theory]
    [InlineData(0x01, -1)]
    [InlineData(0xFF, 1)]
    [InlineData(0x7F, -127)]
    [InlineData(0x80, 128)]
    public unsafe void IntelliMouseWaitsForFourthByteAndUsesTheFullSignedWheelByte(byte wheelByte, int wheel)
    {
        Ps2DecodeState state = default; NativeInputReport report;
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x29, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 2, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, wheelByte, &report));
        Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX);
        Assert.Equal(1, report.DeltaY); Assert.Equal(1u, report.Buttons); Assert.Equal(wheel, report.Wheel);
    }

    [Fact]
    public unsafe void ProtocolSelectionClearsPartialPacketAndStandardFallbackConsumesOnlyThreeBytes()
    {
        Ps2DecodeState state = default; NativeInputReport report;
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 10, &report));
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 0));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 4, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, 0, &report));
        Assert.Equal(4, report.DeltaX); Assert.Equal(0, report.Wheel);
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
        Assert.False(Ps2Decoder.DecodeMouse(ref state, 0, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report)); Assert.Equal(1, report.Wheel);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(4)]
    [InlineData(0xff)]
    public void UnknownDeviceIdIsRejectedInsteadOfClaimingWheelSupport(byte id)
    {
        Ps2DecodeState state = default;
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        Assert.False(Ps2Decoder.SetMouseDeviceId(ref state, id));
        Assert.Equal(0, state.MouseDeviceId); Assert.Equal(0, state.MouseIndex);
    }

    [Fact]
    public unsafe void FourByteOverflowConsumesWheelThenRecoversWithoutPhantomMotion()
    {
        Ps2DecodeState state = default; NativeInputReport report;
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        foreach (byte value in new byte[] { 0xc9, 0x7f, 0x7f }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, 0x08, &report));
        Assert.Equal(Ps2Decoder.Lost, report.Kind); Assert.Equal(1u, report.Buttons);
        Assert.Equal(0, report.DeltaX); Assert.Equal(0, report.DeltaY); Assert.Equal(0, report.Wheel);
        foreach (byte value in new byte[] { 0x08, 2, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, 1, &report));
        Assert.Equal(Ps2Decoder.Pointer, report.Kind); Assert.Equal(2, report.DeltaX); Assert.Equal(-1, report.Wheel);
    }

    [Fact]
    public unsafe void HotLossClearsPartialWheelPacketButPreservesTheNegotiatedFormat()
    {
        Ps2DecodeState state = default; NativeInputReport report;
        Assert.True(Ps2Decoder.SetMouseDeviceId(ref state, 3));
        foreach (byte value in new byte[] { 0x08, 0, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
        Ps2Decoder.ResetMouse(ref state); Assert.Equal(3, state.MouseDeviceId);
        foreach (byte value in new byte[] { 0x08, 3, 0 }) Assert.False(Ps2Decoder.DecodeMouse(ref state, value, &report));
        Assert.True(Ps2Decoder.DecodeMouse(ref state, 0xff, &report));
        Assert.Equal(3, report.DeltaX); Assert.Equal(1, report.Wheel);
    }

    [Fact]
    public void NegotiatedWheelCapabilityAndReportsReachTheActualDesktopInputContract()
    {
        var source = Source(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, Wheel = -1 } }) { Capabilities = 7 });
        Assert.Equal(7u, source.Capabilities);
        Assert.True(source.TryRead(out var wheel)); Assert.Equal(NativeInputKind.Pointer, wheel.Kind); Assert.Equal(-1, wheel.Wheel);
    }

    [Fact]
    public void AvailabilityComesFromActualBackendInitializationCapabilities()
    {
        var backend = new Reports(Keys(0x1E)) { Capabilities = 0 };
        var source = Source(backend);
        Assert.False(source.IsAvailable); Assert.False(source.TryRead(out _)); Assert.Equal(0, source.ReportsRead);
        backend.Capabilities = 1; Assert.True(source.IsAvailable);
        Assert.True(source.TryRead(out var report)); Assert.Equal('a', report.Character);
    }

    [Fact]
    public void WindowsChordReleaseDoesNotOpenStartMenuAndStandaloneWindowsReleaseDoes()
    {
        var chord = Actions(Keys(0xE0, 0x5B, 0x12, 0x92, 0xE0, 0xDB));
        Assert.Single(chord); Assert.Equal(DesktopKey.OpenFiles, chord[0].Key);
        var alone = Actions(Keys(0xE0, 0x5B, 0xE0, 0x5C, 0xE0, 0xDB, 0xE0, 0xDC, 0xE0, 0xDC));
        Assert.Single(alone); Assert.Equal(DesktopKey.StartMenu, alone[0].Key);
        var snap = Actions(Keys(0xE0, 0x5B, 0xE0, 0x4B, 0xE0, 0xCB, 0xE0, 0xDB));
        Assert.Single(snap); Assert.Equal(DesktopKey.SnapLeft, snap[0].Key);
    }

    [Fact]
    public void AltTabSwitchEndsOnlyWhenLastAltIsReleased()
    {
        var actions = Actions(Keys(0x38, 0xE0, 0x38, 0x0F, 0x8F, 0xB8, 0xE0, 0xB8));
        Assert.Equal(new[] { DesktopKey.SwitchWindow, DesktopKey.EndWindowSwitch }, actions.Select(report => report.Key));
        var backwards = Actions(Keys(0x2A, 0x38, 0x0F, 0x8F, 0xB8, 0xAA));
        Assert.Equal(new[] { DesktopKey.SwitchWindowBack, DesktopKey.EndWindowSwitch }, backwards.Select(report => report.Key));
    }

    [Fact]
    public void BrowserReceivesPhysicalCodeRepeatReleaseAndModifierStateWithoutControlText()
    {
        var actions = Actions(Keys(0x2A, 0x1E, 0x1E, 0x9E, 0xAA, 0x1D, 0x1E, 0x9E, 0x9D), browser: true);
        Assert.Equal(9, actions.Length);
        Assert.Equal("KeyA", actions[1].BrowserKey.Code); Assert.Equal("A", actions[1].BrowserKey.Text);
        Assert.False(actions[1].BrowserKey.Repeat); Assert.True(actions[2].BrowserKey.Repeat);
        Assert.Equal("keyup", actions[3].BrowserKey.Type); Assert.Equal("", actions[3].BrowserKey.Text);
        Assert.False(actions[4].BrowserKey.Shift); Assert.True(actions[6].BrowserKey.Control); Assert.Equal("", actions[6].BrowserKey.Text);
        Assert.False(actions[8].BrowserKey.Control);
    }

    [Fact]
    public void LostReportsReleaseBrowserKeysAndDoNotTurnAWindowsBreakIntoAStartGesture()
    {
        var before = Keys(0x1D, 0x1E);
        var backend = new Reports(before.Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 1 } }).ToArray());
        var source = Source(backend, true); var actions = Drain(source);
        Assert.Equal(5, actions.Length);
        Assert.All(actions.Skip(2).Take(2), value => Assert.Equal("keyup", value.BrowserKey.Type));
        Assert.Equal(NativeInputKind.ReportsLost, actions[^1].Kind); Assert.True(actions[^1].LeftButton);
        Assert.Equal(1, source.LostReports);
        var win = Actions(Keys(0xE0, 0x5B).Concat(new[] { new NativeInputReport { Kind = Ps2Decoder.Lost } })
            .Concat(Keys(0xE0, 0xDB)).ToArray());
        Assert.DoesNotContain(win, action => action.Kind == NativeInputKind.Key && action.Key == DesktopKey.StartMenu);
    }

    [Fact]
    public void PointerAccumulationUsesNativePixelsAndClipsBeforeConvertingToLayout()
    {
        var backend = new Reports(new[]
        {
            new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 10, DeltaY = 20, Buttons = 1 },
            new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = int.MinValue, DeltaY = int.MaxValue }
        });
        var source = new Ps2InputSource(backend, 1000, 800, 200, () => new(false, null));
        Assert.True(source.TryRead(out var move)); Assert.Equal(255, move.X); Assert.Equal(210, move.Y); Assert.True(move.LeftButton);
        Assert.True(source.TryRead(out var clipped)); Assert.Equal(0, clipped.X); Assert.Equal(399, clipped.Y);
        var fraction = new Ps2InputSource(new Reports(new[] { new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = 100, DeltaY = 100 } }), 11, 9, 125, () => new(false, null));
        Assert.True(fraction.TryRead(out var edge)); Assert.Equal(7, edge.X); Assert.Equal(6, edge.Y);
    }

    [Fact]
    public void ActualDesktopCancelsCloseCaptureOnLostPacketWithoutSynthesizingClick()
    {
        var desktop = new DesktopSession(new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768)), new MemoryWorkspace());
        var window = desktop.Open(DesktopApplication.Files);
        int closeX = window.Bounds.X + window.Bounds.Width - 20, closeY = window.Bounds.Y + 12;
        var backend = new Reports(new[]
        {
            new NativeInputReport { Kind = Ps2Decoder.Pointer, DeltaX = closeX - 512, DeltaY = closeY - 384, Buttons = 1 },
            new NativeInputReport { Kind = Ps2Decoder.Lost, Buttons = 0 },
            new NativeInputReport { Kind = Ps2Decoder.Pointer, Buttons = 0 }
        });
        var source = new Ps2InputSource(backend, 1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
        foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1000);
        Assert.Single(desktop.Windows);
        backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 1 }); backend.Add(new() { Kind = Ps2Decoder.Pointer, Buttons = 0 });
        foreach (var input in Drain(source)) NativeInput.Dispatch(desktop, input, 1100);
        Assert.Empty(desktop.Windows);
    }

    [Fact]
    public void ActualPs2ByteStreamTypesIntoTheExistingJintBrowserTextarea()
    {
        var canvas = new CpuFramebufferCanvas(new ArrayFramebufferPresenter(1024, 768));
        var desktop = new DesktopSession(canvas, new MemoryWorkspace(), new BrowserService(new InputPage()));
        var window = desktop.Open(DesktopApplication.Browser, "http://input.test/"); desktop.Render();
        int x = window.Bounds.X + 12, y = window.Bounds.Y + 77;
        desktop.Pointer(x, y, true, timestampMs: 100); desktop.Pointer(x, y, false, timestampMs: 101);
        Assert.True(desktop.HasBrowserContentFocus);
        var source = new Ps2InputSource(new Reports(Keys(0x1E, 0x9E, 0x30, 0xB0, 0xE0, 0x4B, 0xE0, 0xCB, 0x2E, 0xAE)),
            1024, 768, 100, () => new(desktop.HasBrowserContentFocus, desktop.ActiveWindow?.Application));
        while (source.TryRead(out var report)) NativeInput.Dispatch(desktop, report, 200);
        var browser = Assert.IsType<BrowserService>(window.BrowserSession);
        Assert.Equal("acb", browser.ExecuteJavaScript("document.getElementById('Entry').value")); browser.Release();
    }

    private sealed class InputPage : IWebTransport
    {
        public WebResponse Get(Uri address, BrowserLimits limits) => new(address, 200, "text/html", "<style>body{margin:0}textarea{display:block;width:300px;height:100px;margin:0;padding:0;border:0}</style><textarea id='Entry'></textarea>");
    }
    private sealed class Reports : IPs2ReportBackend
    {
        private readonly Queue<NativeInputReport> reports;
        public uint Capabilities { get; set; } = 3;
        internal Reports(NativeInputReport[] reports) => this.reports = new(reports);
        public bool TryRead(out NativeInputReport report) => reports.TryDequeue(out report);
        internal void Add(NativeInputReport report) => reports.Enqueue(report);
    }
    private static Ps2InputSource Source(Reports backend, bool browser = false) => new(backend, 1024, 768, 100, () => new(browser, browser ? DesktopApplication.Browser : DesktopApplication.Editor));
    private static NativeInputEvent[] Actions(NativeInputReport[] reports, bool browser = false) => Drain(Source(new(reports), browser));
    private static NativeInputEvent[] Drain(Ps2InputSource source)
    { var result = new List<NativeInputEvent>(); for (int i = 0; i < 1024 && source.TryRead(out var report); i++) result.Add(report); return result.ToArray(); }
    private static unsafe NativeInputReport[] Keys(params byte[] bytes)
    {
        Ps2DecodeState state = default; var result = new List<NativeInputReport>();
        foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeKeyboard(ref state, value, &report)) result.Add(report); }
        return result.ToArray();
    }
    private static unsafe NativeInputReport[] Mouse(params byte[] bytes)
    {
        Ps2DecodeState state = default; var result = new List<NativeInputReport>();
        foreach (byte value in bytes) { NativeInputReport report; if (Ps2Decoder.DecodeMouse(ref state, value, &report)) result.Add(report); }
        return result.ToArray();
    }
}