am: Implement common web applets (#1188)

* am: Implemnet common web applets

This implement parsing of input and output of web applets while making
those close directly.

TODO for the future: Use and hook a web browser.

* Address Ac_K's comments
This commit is contained in:
Thog 2020-05-15 03:56:14 +02:00 committed by GitHub
parent 378259a40a
commit 0ff00bd6d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 463 additions and 2 deletions

View file

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.Applets.Browser;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.Collections.Generic;
@ -14,7 +15,10 @@ namespace Ryujinx.HLE.HOS.Applets
{
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
{ AppletId.Controller, typeof(ControllerApplet) },
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) },
{ AppletId.LibAppletWeb, typeof(BrowserApplet) },
{ AppletId.LibAppletShop, typeof(BrowserApplet) },
{ AppletId.LibAppletOff, typeof(BrowserApplet) }
};
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum BootDisplayKind
{
White,
Offline,
Black,
Share,
Lobby
}
}

View file

@ -0,0 +1,105 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
internal class BrowserApplet : IApplet
{
public event EventHandler AppletStateChanged;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private CommonArguments _commonArguments;
private List<BrowserArgument> _arguments;
private ShimKind _shimKind;
public BrowserApplet(Horizon system) {}
public ResultCode GetResult()
{
return ResultCode.Success;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
_commonArguments = IApplet.ReadStruct<CommonArguments>(_normalSession.Pop());
Logger.PrintStub(LogClass.ServiceAm, $"WebApplet version: 0x{_commonArguments.AppletVersion:x8}");
ReadOnlySpan<byte> webArguments = _normalSession.Pop();
(_shimKind, _arguments) = BrowserArgument.ParseArguments(webArguments);
Logger.PrintStub(LogClass.ServiceAm, $"Web Arguments: {_arguments.Count}");
foreach (BrowserArgument argument in _arguments)
{
Logger.PrintStub(LogClass.ServiceAm, $"{argument.Type}: {argument.GetValue()}");
}
if ((_commonArguments.AppletVersion >= 0x80000 && _shimKind == ShimKind.Web) || (_commonArguments.AppletVersion >= 0x30000 && _shimKind == ShimKind.Share))
{
List<BrowserOutput> result = new List<BrowserOutput>();
result.Add(new BrowserOutput(BrowserOutputType.ExitReason, (uint)WebExitReason.ExitButton));
_normalSession.Push(BuildResponseNew(result));
}
else
{
WebCommonReturnValue result = new WebCommonReturnValue()
{
ExitReason = WebExitReason.ExitButton,
};
_normalSession.Push(BuildResponseOld(result));
}
AppletStateChanged?.Invoke(this, null);
return ResultCode.Success;
}
private byte[] BuildResponseOld(WebCommonReturnValue result)
{
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(result);
return stream.ToArray();
}
}
private byte[] BuildResponseNew(List<BrowserOutput> outputArguments)
{
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(new WebArgHeader
{
Count = (ushort)outputArguments.Count,
ShimKind = _shimKind
});
foreach (BrowserOutput output in outputArguments)
{
output.Write(writer);
}
writer.Write(new byte[0x2000 - writer.BaseStream.Position]);
return stream.ToArray();
}
}
}
}

View file

@ -0,0 +1,133 @@
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
class BrowserArgument
{
public WebArgTLVType Type { get; }
public byte[] Value { get; }
public BrowserArgument(WebArgTLVType type, byte[] value)
{
Type = type;
Value = value;
}
private static readonly Dictionary<WebArgTLVType, Type> _typeRegistry = new Dictionary<WebArgTLVType, Type>
{
{ WebArgTLVType.InitialURL, typeof(string) },
{ WebArgTLVType.CallbackUrl, typeof(string) },
{ WebArgTLVType.CallbackableUrl, typeof(string) },
{ WebArgTLVType.ApplicationId, typeof(ulong) },
{ WebArgTLVType.DocumentPath, typeof(string) },
{ WebArgTLVType.DocumentKind, typeof(DocumentKind) },
{ WebArgTLVType.SystemDataId, typeof(ulong) },
{ WebArgTLVType.Whitelist, typeof(string) },
{ WebArgTLVType.NewsFlag, typeof(bool) },
{ WebArgTLVType.UserID, typeof(UserId) },
{ WebArgTLVType.ScreenShotEnabled, typeof(bool) },
{ WebArgTLVType.EcClientCertEnabled, typeof(bool) },
{ WebArgTLVType.UnknownFlag0x14, typeof(bool) },
{ WebArgTLVType.UnknownFlag0x15, typeof(bool) },
{ WebArgTLVType.PlayReportEnabled, typeof(bool) },
{ WebArgTLVType.BootDisplayKind, typeof(BootDisplayKind) },
{ WebArgTLVType.FooterEnabled, typeof(bool) },
{ WebArgTLVType.PointerEnabled, typeof(bool) },
{ WebArgTLVType.LeftStickMode, typeof(LeftStickMode) },
{ WebArgTLVType.KeyRepeatFrame1, typeof(int) },
{ WebArgTLVType.KeyRepeatFrame2, typeof(int) },
{ WebArgTLVType.BootAsMediaPlayerInverted, typeof(bool) },
{ WebArgTLVType.DisplayUrlKind, typeof(bool) },
{ WebArgTLVType.BootAsMediaPlayer, typeof(bool) },
{ WebArgTLVType.ShopJumpEnabled, typeof(bool) },
{ WebArgTLVType.MediaAutoPlayEnabled, typeof(bool) },
{ WebArgTLVType.LobbyParameter, typeof(string) },
{ WebArgTLVType.JsExtensionEnabled, typeof(bool) },
{ WebArgTLVType.AdditionalCommentText, typeof(string) },
{ WebArgTLVType.TouchEnabledOnContents, typeof(bool) },
{ WebArgTLVType.UserAgentAdditionalString, typeof(string) },
{ WebArgTLVType.MediaPlayerAutoCloseEnabled, typeof(bool) },
{ WebArgTLVType.PageCacheEnabled, typeof(bool) },
{ WebArgTLVType.WebAudioEnabled, typeof(bool) },
{ WebArgTLVType.PageFadeEnabled, typeof(bool) },
{ WebArgTLVType.BootLoadingIconEnabled, typeof(bool) },
{ WebArgTLVType.PageScrollIndicatorEnabled, typeof(bool) },
{ WebArgTLVType.MediaPlayerSpeedControlEnabled, typeof(bool) },
{ WebArgTLVType.OverrideWebAudioVolume, typeof(float) },
{ WebArgTLVType.OverrideMediaAudioVolume, typeof(float) },
{ WebArgTLVType.MediaPlayerUiEnabled, typeof(bool) },
};
public static (ShimKind, List<BrowserArgument>) ParseArguments(ReadOnlySpan<byte> data)
{
List<BrowserArgument> browserArguments = new List<BrowserArgument>();
WebArgHeader header = IApplet.ReadStruct<WebArgHeader>(data.Slice(0, 8));
ReadOnlySpan<byte> rawTLVs = data.Slice(8);
for (int i = 0; i < header.Count; i++)
{
WebArgTLV tlv = IApplet.ReadStruct<WebArgTLV>(rawTLVs);
ReadOnlySpan<byte> tlvData = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>(), tlv.Size);
browserArguments.Add(new BrowserArgument((WebArgTLVType)tlv.Type, tlvData.ToArray()));
rawTLVs = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>() + tlv.Size);
}
return (header.ShimKind, browserArguments);
}
public object GetValue()
{
if (_typeRegistry.TryGetValue(Type, out Type valueType))
{
if (valueType == typeof(string))
{
return Encoding.UTF8.GetString(Value);
}
else if (valueType == typeof(bool))
{
return Value[0] == 1;
}
else if (valueType == typeof(uint))
{
return BitConverter.ToUInt32(Value);
}
else if (valueType == typeof(int))
{
return BitConverter.ToInt32(Value);
}
else if (valueType == typeof(ulong))
{
return BitConverter.ToUInt64(Value);
}
else if (valueType == typeof(long))
{
return BitConverter.ToInt64(Value);
}
else if (valueType == typeof(float))
{
return BitConverter.ToSingle(Value);
}
else if (valueType == typeof(UserId))
{
return new UserId(Value);
}
else if (valueType.IsEnum)
{
return Enum.ToObject(valueType, BitConverter.ToInt32(Value));
}
return $"{valueType.Name} parsing not implemented";
}
return $"Unknown value format (raw length: {Value.Length})";
}
}
}

View file

@ -0,0 +1,47 @@
using Ryujinx.Common;
using System;
using System.IO;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
class BrowserOutput
{
public BrowserOutputType Type { get; }
public byte[] Value { get; }
public BrowserOutput(BrowserOutputType type, byte[] value)
{
Type = type;
Value = value;
}
public BrowserOutput(BrowserOutputType type, uint value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public BrowserOutput(BrowserOutputType type, ulong value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public BrowserOutput(BrowserOutputType type, bool value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public void Write(BinaryWriter writer)
{
writer.WriteStruct(new WebArgTLV
{
Type = (ushort)Type,
Size = (ushort)Value.Length
});
writer.Write(Value);
}
}
}

View file

@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum BrowserOutputType : ushort
{
ExitReason = 0x1,
LastUrl = 0x2,
LastUrlSize = 0x3,
SharePostResult = 0x4,
PostServiceName = 0x5,
PostServiceNameSize = 0x6,
PostId = 0x7,
MediaPlayerAutoClosedByCompletion = 0x8
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum DocumentKind
{
OfflineHtmlPage = 1,
ApplicationLegalInformation,
SystemDataPage
}
}

View file

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum LeftStickMode
{
Pointer = 0,
Cursor
}
}

View file

@ -0,0 +1,13 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public enum ShimKind : uint
{
Shop = 1,
Login,
Offline,
Share,
Web,
Wifi,
Lobby
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public struct WebArgHeader
{
public ushort Count;
public ushort Padding;
public ShimKind ShimKind;
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public struct WebArgTLV
{
public ushort Type;
public ushort Size;
public uint Padding;
}
}

View file

@ -0,0 +1,62 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum WebArgTLVType : ushort
{
InitialURL = 0x1,
CallbackUrl = 0x3,
CallbackableUrl = 0x4,
ApplicationId = 0x5,
DocumentPath = 0x6,
DocumentKind = 0x7,
SystemDataId = 0x8,
ShareStartPage = 0x9,
Whitelist = 0xA,
NewsFlag = 0xB,
UserID = 0xE,
AlbumEntry0 = 0xF,
ScreenShotEnabled = 0x10,
EcClientCertEnabled = 0x11,
PlayReportEnabled = 0x13,
UnknownFlag0x14 = 0x14,
UnknownFlag0x15 = 0x15,
BootDisplayKind = 0x17,
BackgroundKind = 0x18,
FooterEnabled = 0x19,
PointerEnabled = 0x1A,
LeftStickMode = 0x1B,
KeyRepeatFrame1 = 0x1C,
KeyRepeatFrame2 = 0x1D,
BootAsMediaPlayerInverted = 0x1E,
DisplayUrlKind = 0x1F,
BootAsMediaPlayer = 0x21,
ShopJumpEnabled = 0x22,
MediaAutoPlayEnabled = 0x23,
LobbyParameter = 0x24,
ApplicationAlbumEntry = 0x26,
JsExtensionEnabled = 0x27,
AdditionalCommentText = 0x28,
TouchEnabledOnContents = 0x29,
UserAgentAdditionalString = 0x2A,
AdditionalMediaData0 = 0x2B,
MediaPlayerAutoCloseEnabled = 0x2C,
PageCacheEnabled = 0x2D,
WebAudioEnabled = 0x2E,
FooterFixedKind = 0x32,
PageFadeEnabled = 0x33,
MediaCreatorApplicationRatingAge = 0x34,
BootLoadingIconEnabled = 0x35,
PageScrollIndicatorEnabled = 0x36,
MediaPlayerSpeedControlEnabled = 0x37,
AlbumEntry1 = 0x38,
AlbumEntry2 = 0x39,
AlbumEntry3 = 0x3A,
AdditionalMediaData1 = 0x3B,
AdditionalMediaData2 = 0x3C,
AdditionalMediaData3 = 0x3D,
BootFooterButton = 0x3E,
OverrideWebAudioVolume = 0x3F,
OverrideMediaAudioVolume = 0x40,
BootMode = 0x41,
MediaPlayerUiEnabled = 0x43
}
}

View file

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public unsafe struct WebCommonReturnValue
{
public WebExitReason ExitReason;
public uint Padding;
public fixed byte LastUrl[0x1000];
public ulong LastUrlSize;
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public enum WebExitReason : uint
{
ExitButton,
BackButton,
Requested,
LastUrl,
ErrorDialog = 7
}
}

View file

@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
[StructLayout(LayoutKind.Sequential, Pack = 8)]
struct CommonArguments
{
public uint Version;
public uint StructureSize;
public uint AppletVersion;
public uint ThemeColor;
[MarshalAs(UnmanagedType.I1)]
public bool PlayStartupSound;
public ulong SystemTicks;
}
}