From cbef61fc2a6ff46664964704ce0f273e45ef40a7 Mon Sep 17 00:00:00 2001 From: gibbed Date: Tue, 14 Nov 2017 14:02:25 -0600 Subject: [PATCH] Properly handle encoding of strings in Steam API. Fixes #5. --- SAM.API/NativeStrings.cs | 141 ++++++++++++++++++++++++++ SAM.API/SAM.API.csproj | 3 + SAM.API/Wrappers/SteamApps001.cs | 30 +++--- SAM.API/Wrappers/SteamApps003.cs | 2 +- SAM.API/Wrappers/SteamClient009.cs | 84 ++++++++------- SAM.API/Wrappers/SteamUserStats007.cs | 108 +++++++++++++------- SAM.API/Wrappers/SteamUtils005.cs | 5 +- 7 files changed, 285 insertions(+), 88 deletions(-) create mode 100644 SAM.API/NativeStrings.cs diff --git a/SAM.API/NativeStrings.cs b/SAM.API/NativeStrings.cs new file mode 100644 index 0000000..8dc0687 --- /dev/null +++ b/SAM.API/NativeStrings.cs @@ -0,0 +1,141 @@ +/* Copyright (c) 2017 Rick (rick 'at' gibbed 'dot' us) + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would + * be appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not + * be misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source + * distribution. + */ + +using System; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace SAM.API +{ + internal class NativeStrings + { + public sealed class StringHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal StringHandle(IntPtr preexistingHandle, bool ownsHandle) + : base(ownsHandle) + { + this.SetHandle(preexistingHandle); + } + + public IntPtr Handle + { + get { return this.handle; } + } + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + { + Marshal.FreeHGlobal(handle); + handle = IntPtr.Zero; + return true; + } + + return false; + } + } + + public static unsafe StringHandle StringToStringHandle(string value) + { + if (value == null) + { + return new StringHandle(IntPtr.Zero, true); + } + + var bytes = Encoding.UTF8.GetBytes(value); + var length = bytes.Length; + + var p = Marshal.AllocHGlobal(length + 1); + Marshal.Copy(bytes, 0, p, bytes.Length); + ((byte*)p)[length] = 0; + return new StringHandle(p, true); + } + + public static unsafe string PointerToString(sbyte* bytes) + { + if (bytes == null) + { + return null; + } + + int running = 0; + + var b = bytes; + if (*b == 0) + { + return string.Empty; + } + + while ((*b++) != 0) + { + running++; + } + + return new string(bytes, 0, running, Encoding.UTF8); + } + + public static unsafe string PointerToString(byte* bytes) + { + return PointerToString((sbyte*)bytes); + } + + public static unsafe string PointerToString(IntPtr nativeData) + { + return PointerToString((sbyte*)nativeData.ToPointer()); + } + + public static unsafe string PointerToString(sbyte* bytes, int length) + { + if (bytes == null) + { + return null; + } + + int running = 0; + + var b = bytes; + if (length == 0 || *b == 0) + { + return string.Empty; + } + + while ((*b++) != 0 && + running < length) + { + running++; + } + + return new string(bytes, 0, running, Encoding.UTF8); + } + + public static unsafe string PointerToString(byte* bytes, int length) + { + return PointerToString((sbyte*)bytes, length); + } + + public static unsafe string PointerToString(IntPtr nativeData, int length) + { + return PointerToString((sbyte*)nativeData.ToPointer(), length); + } + } +} diff --git a/SAM.API/SAM.API.csproj b/SAM.API/SAM.API.csproj index 9a8015c..71b3026 100644 --- a/SAM.API/SAM.API.csproj +++ b/SAM.API/SAM.API.csproj @@ -39,6 +39,7 @@ 4 x86 AllRules.ruleset + true none @@ -50,6 +51,7 @@ x86 false AllRules.ruleset + true @@ -69,6 +71,7 @@ + diff --git a/SAM.API/Wrappers/SteamApps001.cs b/SAM.API/Wrappers/SteamApps001.cs index d82eee4..02d0534 100644 --- a/SAM.API/Wrappers/SteamApps001.cs +++ b/SAM.API/Wrappers/SteamApps001.cs @@ -22,7 +22,6 @@ using System; using System.Runtime.InteropServices; -using System.Text; using SAM.API.Interfaces; namespace SAM.API.Wrappers @@ -34,22 +33,27 @@ namespace SAM.API.Wrappers private delegate int NativeGetAppData( IntPtr self, uint appId, - string key, - StringBuilder valueBuffer, + IntPtr key, + IntPtr value, int valueLength); public string GetAppData(uint appId, string key) { - var sb = new StringBuilder(); - sb.EnsureCapacity(1024); - int result = this.Call( - this.Functions.GetAppData, - this.ObjectAddress, - appId, - key, - sb, - sb.Capacity); - return result == 0 ? null : sb.ToString(); + using (var nativeHandle = NativeStrings.StringToStringHandle(key)) + { + const int valueLength = 1024; + var valuePointer = Marshal.AllocHGlobal(valueLength); + int result = this.Call( + this.Functions.GetAppData, + this.ObjectAddress, + appId, + nativeHandle.Handle, + valuePointer, + valueLength); + var value = result == 0 ? null : NativeStrings.PointerToString(valuePointer, valueLength); + Marshal.FreeHGlobal(valuePointer); + return value; + } } #endregion } diff --git a/SAM.API/Wrappers/SteamApps003.cs b/SAM.API/Wrappers/SteamApps003.cs index fcc8b44..db5caa1 100644 --- a/SAM.API/Wrappers/SteamApps003.cs +++ b/SAM.API/Wrappers/SteamApps003.cs @@ -48,7 +48,7 @@ namespace SAM.API.Wrappers var languagePointer = this.Call( this.Functions.GetCurrentGameLanguage, this.ObjectAddress); - return Marshal.PtrToStringAnsi(languagePointer); + return NativeStrings.PointerToString(languagePointer); } #endregion } diff --git a/SAM.API/Wrappers/SteamClient009.cs b/SAM.API/Wrappers/SteamClient009.cs index 8f050e7..a37e558 100644 --- a/SAM.API/Wrappers/SteamClient009.cs +++ b/SAM.API/Wrappers/SteamClient009.cs @@ -95,20 +95,23 @@ namespace SAM.API.Wrappers #region GetISteamUser [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr NativeGetISteamUser(IntPtr self, int user, int pipe, string version); + private delegate IntPtr NativeGetISteamUser(IntPtr self, int user, int pipe, IntPtr version); private TClass GetISteamUser(int user, int pipe, string version) where TClass : INativeWrapper, new() { - IntPtr address = this.Call( - this.Functions.GetISteamUser, - this.ObjectAddress, - user, - pipe, - version); - var result = new TClass(); - result.SetupFunctions(address); - return result; + using (var nativeVersion = NativeStrings.StringToStringHandle(version)) + { + IntPtr address = this.Call( + this.Functions.GetISteamUser, + this.ObjectAddress, + user, + pipe, + nativeVersion.Handle); + var result = new TClass(); + result.SetupFunctions(address); + return result; + } } #endregion @@ -121,20 +124,23 @@ namespace SAM.API.Wrappers #region GetISteamUserStats [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr NativeGetISteamUserStats(IntPtr self, int user, int pipe, string version); + private delegate IntPtr NativeGetISteamUserStats(IntPtr self, int user, int pipe, IntPtr version); private TClass GetISteamUserStats(int user, int pipe, string version) where TClass : INativeWrapper, new() { - IntPtr address = this.Call( - this.Functions.GetISteamUserStats, - this.ObjectAddress, - user, - pipe, - version); - var result = new TClass(); - result.SetupFunctions(address); - return result; + using (var nativeVersion = NativeStrings.StringToStringHandle(version)) + { + IntPtr address = this.Call( + this.Functions.GetISteamUserStats, + this.ObjectAddress, + user, + pipe, + nativeVersion.Handle); + var result = new TClass(); + result.SetupFunctions(address); + return result; + } } #endregion @@ -147,19 +153,22 @@ namespace SAM.API.Wrappers #region GetISteamUtils [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr NativeGetISteamUtils(IntPtr self, int pipe, string version); + private delegate IntPtr NativeGetISteamUtils(IntPtr self, int pipe, IntPtr version); public TClass GetISteamUtils(int pipe, string version) where TClass : INativeWrapper, new() { - IntPtr address = this.Call( - this.Functions.GetISteamUtils, - this.ObjectAddress, - pipe, - version); - var result = new TClass(); - result.SetupFunctions(address); - return result; + using (var nativeVersion = NativeStrings.StringToStringHandle(version)) + { + IntPtr address = this.Call( + this.Functions.GetISteamUtils, + this.ObjectAddress, + pipe, + nativeVersion.Handle); + var result = new TClass(); + result.SetupFunctions(address); + return result; + } } #endregion @@ -171,15 +180,22 @@ namespace SAM.API.Wrappers #endregion #region GetISteamApps - private delegate IntPtr NativeGetISteamApps(int user, int pipe, string version); + private delegate IntPtr NativeGetISteamApps(int user, int pipe, IntPtr version); private TClass GetISteamApps(int user, int pipe, string version) where TClass : INativeWrapper, new() { - IntPtr address = this.Call(this.Functions.GetISteamApps, user, pipe, version); - var result = new TClass(); - result.SetupFunctions(address); - return result; + using (var nativeVersion = NativeStrings.StringToStringHandle(version)) + { + IntPtr address = this.Call( + this.Functions.GetISteamApps, + user, + pipe, + nativeVersion.Handle); + var result = new TClass(); + result.SetupFunctions(address); + return result; + } } #endregion diff --git a/SAM.API/Wrappers/SteamUserStats007.cs b/SAM.API/Wrappers/SteamUserStats007.cs index b38fcdb..a53af21 100644 --- a/SAM.API/Wrappers/SteamUserStats007.cs +++ b/SAM.API/Wrappers/SteamUserStats007.cs @@ -42,54 +42,66 @@ namespace SAM.API.Wrappers #region GetStatValue (int) [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeGetStatInt(IntPtr self, string name, out int data); + private delegate bool NativeGetStatInt(IntPtr self, IntPtr name, out int data); public bool GetStatValue(string name, out int value) { - var call = this.GetFunction(this.Functions.GetStatInteger); - return call(this.ObjectAddress, name, out value); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + var call = this.GetFunction(this.Functions.GetStatInteger); + return call(this.ObjectAddress, nativeName.Handle, out value); + } } #endregion #region GetStatValue (float) [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeGetStatFloat(IntPtr self, string name, out float data); + private delegate bool NativeGetStatFloat(IntPtr self, IntPtr name, out float data); public bool GetStatValue(string name, out float value) { - var call = this.GetFunction(this.Functions.GetStatFloat); - return call(this.ObjectAddress, name, out value); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + var call = this.GetFunction(this.Functions.GetStatFloat); + return call(this.ObjectAddress, nativeName.Handle, out value); + } } #endregion #region SetStatValue (int) [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeSetStatInt(IntPtr self, string name, int data); + private delegate bool NativeSetStatInt(IntPtr self, IntPtr name, int data); public bool SetStatValue(string name, int value) { - return this.Call( - this.Functions.SetStatInteger, - this.ObjectAddress, - name, - value); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + return this.Call( + this.Functions.SetStatInteger, + this.ObjectAddress, + nativeName.Handle, + value); + } } #endregion #region SetStatValue (float) [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeSetStatFloat(IntPtr self, string name, float data); + private delegate bool NativeSetStatFloat(IntPtr self, IntPtr name, float data); public bool SetStatValue(string name, float value) { - return this.Call( - this.Functions.SetStatFloat, - this.ObjectAddress, - name, - value); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + return this.Call( + this.Functions.SetStatFloat, + this.ObjectAddress, + nativeName.Handle, + value); + } } #endregion @@ -98,33 +110,45 @@ namespace SAM.API.Wrappers [return: MarshalAs(UnmanagedType.I1)] private delegate bool NativeGetAchievement( IntPtr self, - string name, + IntPtr name, [MarshalAs(UnmanagedType.I1)] out bool isAchieved); public bool GetAchievementState(string name, out bool isAchieved) { - var call = this.GetFunction(this.Functions.GetAchievement); - return call(this.ObjectAddress, name, out isAchieved); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + var call = this.GetFunction(this.Functions.GetAchievement); + return call(this.ObjectAddress, nativeName.Handle, out isAchieved); + } } #endregion #region SetAchievementState [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeSetAchievement(IntPtr self, string name); + private delegate bool NativeSetAchievement(IntPtr self, IntPtr name); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NativeClearAchievement(IntPtr self, string name); + private delegate bool NativeClearAchievement(IntPtr self, IntPtr name); public bool SetAchievement(string name, bool state) { - if (state == false) + using (var nativeName = NativeStrings.StringToStringHandle(name)) { - return this.Call(this.Functions.ClearAchievement, this.ObjectAddress, name); - } + if (state == false) + { + return this.Call( + this.Functions.ClearAchievement, + this.ObjectAddress, + nativeName.Handle); + } - return this.Call(this.Functions.SetAchievement, this.ObjectAddress, name); + return this.Call( + this.Functions.SetAchievement, + this.ObjectAddress, + nativeName.Handle); + } } #endregion @@ -141,28 +165,36 @@ namespace SAM.API.Wrappers #region GetAchievementIcon [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate int NativeGetAchievementIcon(IntPtr self, string name); + private delegate int NativeGetAchievementIcon(IntPtr self, IntPtr name); public int GetAchievementIcon(string name) { - return this.Call( - this.Functions.GetAchievementIcon, - this.ObjectAddress, - name); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + { + return this.Call( + this.Functions.GetAchievementIcon, + this.ObjectAddress, + nativeName.Handle); + } } #endregion #region GetAchievementDisplayAttribute [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate string NativeGetAchievementDisplayAttribute(IntPtr self, string name, string key); + private delegate IntPtr NativeGetAchievementDisplayAttribute(IntPtr self, IntPtr name, IntPtr key); public string GetAchievementDisplayAttribute(string name, string key) { - return this.Call( - this.Functions.GetAchievementDisplayAttribute, - this.ObjectAddress, - name, - key); + using (var nativeName = NativeStrings.StringToStringHandle(name)) + using (var nativeKey = NativeStrings.StringToStringHandle(key)) + { + var result = this.Call( + this.Functions.GetAchievementDisplayAttribute, + this.ObjectAddress, + nativeName.Handle, + nativeKey.Handle); + return NativeStrings.PointerToString(result); + } } #endregion diff --git a/SAM.API/Wrappers/SteamUtils005.cs b/SAM.API/Wrappers/SteamUtils005.cs index cded620..55edc4f 100644 --- a/SAM.API/Wrappers/SteamUtils005.cs +++ b/SAM.API/Wrappers/SteamUtils005.cs @@ -40,11 +40,12 @@ namespace SAM.API.Wrappers #region GetIPCountry [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate string NativeGetIPCountry(IntPtr self); + private delegate IntPtr NativeGetIPCountry(IntPtr self); public string GetIPCountry() { - return this.Call(this.Functions.GetIPCountry, this.ObjectAddress); + var result = this.Call(this.Functions.GetIPCountry, this.ObjectAddress); + return NativeStrings.PointerToString(result); } #endregion