Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Fable.Transforms/Beam/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,14 @@ let private convert
| "FromBase64String", [ arg ] ->
Helper.LibCall(com, "fable_convert", "from_base64", t, [ arg ], ?loc = r)
|> Some
| "ToHexString", ([ _ ] | [ _; _; _ ]) ->
Helper.LibCall(com, "fable_convert", "to_hex_string", t, args, ?loc = r) |> Some
| "ToHexStringLower", ([ _ ] | [ _; _; _ ]) ->
Helper.LibCall(com, "fable_convert", "to_hex_string_lower", t, args, ?loc = r)
|> Some
| "FromHexString", [ arg ] ->
Helper.LibCall(com, "fable_convert", "from_hex_string", t, [ arg ], ?loc = r)
|> Some
| _ -> None

/// Beam-specific List module replacements.
Expand Down
7 changes: 7 additions & 0 deletions src/Fable.Transforms/Dart/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2828,6 +2828,13 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (
?loc = r
)
|> Some
| "ToHexString"
| "ToHexStringLower" ->
Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| "FromHexString" when List.isSingle args ->
Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| _ -> None

let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
Expand Down
7 changes: 7 additions & 0 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3026,6 +3026,13 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (

Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| "ToHexString"
| "ToHexStringLower" ->
Helper.LibCall(com, "Convert", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| "FromHexString" when List.isSingle args ->
Helper.LibCall(com, "Convert", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| _ -> None

let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
Expand Down
21 changes: 20 additions & 1 deletion src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3432,6 +3432,13 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option
Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some

// The library decodes into a Uint8Array regardless of the typedArrays option
let private decodedBytes (com: ICompiler) r t expr =
if com.Options.TypedArrays then
expr
else
toArray r t expr

let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) =
match i.CompiledName with
| "ToSByte"
Expand All @@ -3456,7 +3463,19 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (
if not (List.isSingle args) then
WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r

Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r)
let expr =
Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r)

match i.CompiledName with
| "FromBase64String" -> decodedBytes com r t expr |> Some
| _ -> Some expr
| "ToHexString"
| "ToHexStringLower" ->
Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| "FromHexString" when List.isSingle args ->
Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r)
|> decodedBytes com r t
|> Some
| _ -> None

Expand Down
71 changes: 71 additions & 0 deletions src/fable-library-beam/fable_convert.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
to_string_with_base/3,
to_base64/1,
from_base64/1,
to_hex_string/1,
to_hex_string/3,
to_hex_string_lower/1,
to_hex_string_lower/3,
from_hex_string/1,
boolean_parse/1,
boolean_try_parse/2,
int_to_string_with_format/2,
Expand All @@ -28,6 +33,11 @@
-spec to_string_with_base(integer(), integer(), integer()) -> binary().
-spec to_base64(tuple() | list() | binary()) -> binary().
-spec from_base64(binary()) -> tuple().
-spec to_hex_string(tuple() | list() | binary() | reference() | undefined) -> binary().
-spec to_hex_string(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary().
-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined) -> binary().
-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary().
-spec from_hex_string(binary() | undefined) -> tuple().
-spec boolean_parse(binary()) -> boolean().
-spec boolean_try_parse(binary(), reference()) -> boolean().
-spec int_to_string_with_format(integer(), binary()) -> binary().
Expand Down Expand Up @@ -196,6 +206,67 @@ to_base64(Bin) when is_binary(Bin) ->
from_base64(Str) when is_binary(Str) ->
fable_utils:new_byte_array(binary_to_list(base64:decode(Str))).

to_hex_string(Bytes) ->
binary:encode_hex(hex_input(Bytes)).

to_hex_string(Bytes, Offset, Length) ->
binary:encode_hex(hex_input(Bytes, Offset, Length)).

%% binary:encode_hex/2 with lowercase needs OTP 26
to_hex_string_lower(Bytes) ->
string:lowercase(to_hex_string(Bytes)).

to_hex_string_lower(Bytes, Offset, Length) ->
string:lowercase(to_hex_string(Bytes, Offset, Length)).

hex_input(undefined) ->
argument_null(<<"inArray">>);
hex_input({byte_array, _, _} = BA) ->
list_to_binary(fable_utils:byte_array_to_list(BA));
hex_input(Bytes) when is_list(Bytes) ->
list_to_binary(Bytes);
hex_input(Bin) when is_binary(Bin) ->
Bin;
hex_input(Ref) when is_reference(Ref) ->
hex_input(get(Ref)).

hex_input(Bytes, Offset, Length) ->
Bin = hex_input(Bytes),
if
Length < 0 -> argument_out_of_range(<<"length">>, <<"Non-negative number required.">>);
Offset < 0 -> argument_out_of_range(<<"offset">>, <<"Non-negative number required.">>);
Offset > byte_size(Bin) - Length ->
argument_out_of_range(<<"offset">>, <<"Specified argument was out of the range of valid values.">>);
true -> binary:part(Bin, Offset, Length)
end.

from_hex_string(undefined) ->
argument_null(<<"s">>);
from_hex_string(Str) when byte_size(Str) rem 2 =/= 0 ->
bad_hex_string(<<"The input is not a valid hex string as its length is not a multiple of 2.">>);
from_hex_string(Str) ->
try binary:decode_hex(Str) of
Bin -> fable_utils:new_byte_array(binary_to_list(Bin))
catch
error:badarg ->
bad_hex_string(<<"The input is not a valid hex string as it contains a non-hex character.">>)
end.

argument_null(ParamName) ->
erlang:error(#{
exn_type => argument_null_exception,
message => fable_utils:argument_exception_message(<<"Value cannot be null.">>, ParamName)
}).

argument_out_of_range(ParamName, Message) ->
erlang:error(#{
exn_type => argument_out_of_range_exception,
message => fable_utils:argument_exception_message(Message, ParamName)
}).

bad_hex_string(Message) ->
erlang:error(#{exn_type => format_exception, message => Message}).

%% TryParse: returns bool and sets out-param via put(OutRef, Value).
%% F# uses out-parameter pattern: result = TryParse(str, &outRef).
try_parse_int(Bin, OutRef) when is_binary(Bin) ->
Expand Down
40 changes: 40 additions & 0 deletions src/fable-library-dart/String.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// ignore_for_file: file_names, constant_identifier_names

import 'System.dart' as system;

class StringComparison {
static const CurrentCulture = 0;
static const CurrentCultureIgnoreCase = 1;
Expand Down Expand Up @@ -280,3 +282,41 @@ String mapIndexed(int Function(int, int) mapping, String str) {
return mapping(index, rune);
}));
}

String toHexStringLower(List<int> inArray, [int? offset, int? length]) {
offset ??= 0;
length ??= inArray.length - offset;
if (length < 0) {
throw system.ArgumentOutOfRangeException_$ctor_Z384F8060(
'length', 'Non-negative number required.');
}
if (offset < 0) {
throw system.ArgumentOutOfRangeException_$ctor_Z384F8060(
'offset', 'Non-negative number required.');
}
if (offset > inArray.length - length) {
throw system.ArgumentOutOfRangeException_$ctor_Z721C83C5('offset');
}
return inArray
.sublist(offset, offset + length)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
}

String toHexString(List<int> inArray, [int? offset, int? length]) {
return toHexStringLower(inArray, offset, length).toUpperCase();
}

List<int> fromHexString(String s) {
if (s.length % 2 != 0) {
throw system.FormatException_$ctor_Z721C83C5(
'The input is not a valid hex string as its length is not a multiple of 2.');
}
if (!RegExp(r'^[0-9A-Fa-f]*$').hasMatch(s)) {
throw system.FormatException_$ctor_Z721C83C5(
'The input is not a valid hex string as it contains a non-hex character.');
}
return List<int>.generate(
s.length ~/ 2, (i) => int.parse(s.substring(2 * i, 2 * i + 2), radix: 16),
growable: false);
}
55 changes: 55 additions & 0 deletions src/fable-library-py/fable_library/convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

from typing import SupportsInt

from .array_ import Array
from .core import byte
from .system import (
ArgumentNullException__ctor_Z721C83C5,
ArgumentOutOfRangeException__ctor_Z384F8060,
ArgumentOutOfRangeException__ctor_Z721C83C5,
FormatException__ctor_Z721C83C5,
)


_HEX_DIGITS = frozenset("0123456789abcdefABCDEF")


def _hex_input(in_array: Array[byte], offset: SupportsInt | None, length: SupportsInt | None) -> bytes:
if in_array is None:
raise ArgumentNullException__ctor_Z721C83C5("inArray")
data = bytes(in_array)
start = 0 if offset is None else int(offset)
count = len(data) - start if length is None else int(length)
if count < 0:
raise ArgumentOutOfRangeException__ctor_Z384F8060("length", "Non-negative number required.")
if start < 0:
raise ArgumentOutOfRangeException__ctor_Z384F8060("offset", "Non-negative number required.")
if start > len(data) - count:
raise ArgumentOutOfRangeException__ctor_Z721C83C5("offset")
return data[start : start + count]


def to_hex_string(in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None) -> str:
"""Convert a byte array, or length bytes of it from offset, to uppercase hex (Convert.ToHexString)."""
return _hex_input(in_array, offset, length).hex().upper()


def to_hex_string_lower(
in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None
) -> str:
"""Convert a byte array, or length bytes of it from offset, to lowercase hex (Convert.ToHexStringLower)."""
return _hex_input(in_array, offset, length).hex()


def from_hex_string(s: str) -> Array[byte]:
"""Convert a hex string of either case to a byte array (Convert.FromHexString)."""
if s is None:
raise ArgumentNullException__ctor_Z721C83C5("s")
if len(s) % 2 != 0:
raise FormatException__ctor_Z721C83C5(
"The input is not a valid hex string as its length is not a multiple of 2."
)
if not _HEX_DIGITS.issuperset(s):
raise FormatException__ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character.")
return Array[byte](byte(x) for x in bytes.fromhex(s))
3 changes: 3 additions & 0 deletions src/fable-library-rust/src/Convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ pub mod Convert_ {
}
}
let chars = s.as_bytes();
if chars.len() % 2 != 0 {
panic!("The input is not a valid hex string as its length is not a multiple of 2.");
}
let bytes = chars
.chunks_exact(2)
.map(|x| decode(x[0]) << 4 | decode(x[1]))
Expand Down
44 changes: 44 additions & 0 deletions src/fable-library-ts/String.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { toString as dateToString } from "./Date.ts";
import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision, toRadix } from "./Numeric.ts";
import { escape } from "./RegExp.ts";
import { ArgumentNullException_$ctor_Z721C83C5, ArgumentOutOfRangeException_$ctor_Z384F8060, ArgumentOutOfRangeException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts";
import { toString } from "./Types.ts";
import { Exception } from "./Util.ts";

Expand Down Expand Up @@ -683,6 +684,49 @@ export function fromBase64String(b64Encoded: string): number[] {
return bytes as any as number[];
}

export function toHexString(inArray: ArrayLike<number>, offset?: number, length?: number): string {
if (inArray == null) {
throw ArgumentNullException_$ctor_Z721C83C5("inArray");
}
const start = offset ?? 0;
const count = length ?? inArray.length - start;
if (count < 0) {
throw ArgumentOutOfRangeException_$ctor_Z384F8060("length", "Non-negative number required.");
}
if (start < 0) {
throw ArgumentOutOfRangeException_$ctor_Z384F8060("offset", "Non-negative number required.");
}
if (start > inArray.length - count) {
throw ArgumentOutOfRangeException_$ctor_Z721C83C5("offset");
}
let str = "";
for (let i = start; i < start + count; i++) {
str += inArray[i].toString(16).padStart(2, "0");
}
return str.toUpperCase();
}

export function toHexStringLower(inArray: ArrayLike<number>, offset?: number, length?: number): string {
return toHexString(inArray, offset, length).toLowerCase();
}

export function fromHexString(s: string): number[] {
if (s == null) {
throw ArgumentNullException_$ctor_Z721C83C5("s");
}
if (s.length % 2 !== 0) {
throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as its length is not a multiple of 2.");
}
if (!/^[0-9A-Fa-f]*$/.test(s)) {
throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character.");
}
const bytes = new Uint8Array(s.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(s.substring(2 * i, 2 * i + 2), 16);
}
return bytes as any as number[];
}

function pad(str: string, len: number, ch?: string, isRight?: boolean) {
ch = ch || " ";
len = len - str.length;
Expand Down
49 changes: 49 additions & 0 deletions tests/Beam/EncodingTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,55 @@ let ``test Base64 empty array works`` () =
let encoded = Convert.ToBase64String(bytes)
encoded |> equal ""

// --- Hex ---

[<Fact>]
let ``test Convert.ToHexString works`` () =
let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |]
Convert.ToHexString(bytes) |> equal "FAFBFCFDFE"
Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10"
Convert.ToHexString([||]) |> equal ""
Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD"
Convert.ToHexString(bytes, 5, 0) |> equal ""
Convert.ToHexString(List.toArray [ 200uy; 3uy ]) |> equal "C803"

[<Fact>]
let ``test Convert.ToHexStringLower works`` () =
let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |]
Convert.ToHexStringLower(bytes) |> equal "fafbfcfdfe"
Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10"
Convert.ToHexStringLower([||]) |> equal ""
Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd"

[<Fact>]
let ``test Convert.ToHexString checks its arguments`` () =
let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |]
raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException"
raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException"
raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException"
raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException"
raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException"
raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException"
raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException"
raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException"
raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException"

[<Fact>]
let ``test Convert.FromHexString works`` () =
Convert.FromHexString("fafBFCFDFE") |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |]
Convert.FromHexString("0123456789abcdefABCDEF")
|> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |]
Convert.FromHexString("") |> equal [||]

[<Fact>]
let ``test Convert.FromHexString checks its input`` () =
raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException"
raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException"
raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException"
raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException"
raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException"
raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException"

// --- Guid ---

[<Fact>]
Expand Down
Loading
Loading