Skip to content

Commit 5555dee

Browse files
committed
Merge pull request #2134 from bettio/inet-parse-ntoa
Add `inet` IP address parse and ntoa functions These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents 68a8652 + ac660eb commit 5555dee

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7878
- Added `network:sta_status/0` to get the current connection state of the sta interface.
7979
- Added ESP32 `-DATOMVM_ELIXIR_SUPPORT=on` configuration option
8080
- Added support for ESP32 development builds to include NVS partition data at build time
81+
- Added missing `inet` functions: `ntoa/1`, `parse_address/1`, `parse_ipv4_address/1`,
82+
`parse_ipv4strict_address/1`
8183

8284
### Changed
8385

libs/estdlib/src/inet.erl

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
-module(inet).
2222

2323
-export([port/1, close/1, sockname/1, peername/1, getaddr/2]).
24+
-export([ntoa/1, parse_address/1, parse_ipv4_address/1, parse_ipv4strict_address/1]).
2425

2526
-include("inet-priv.hrl").
2627

@@ -146,3 +147,121 @@ getaddr(Name, Family) when is_list(Name) ->
146147
end;
147148
getaddr(_Name, _Family) ->
148149
{error, einval}.
150+
151+
%%-----------------------------------------------------------------------------
152+
%% @param IpAddress an IPv4 address tuple
153+
%% @returns a string representation of the IP address, or `{error, einval}'
154+
%% if the argument is not a valid IPv4 address.
155+
%% @doc Convert an IPv4 address to a string.
156+
%%
157+
%% This function converts an `ip4_address()' tuple to its dotted-decimal
158+
%% string representation, for example `{192, 168, 0, 1}' becomes
159+
%% `"192.168.0.1"'.
160+
%%
161+
%% <b>Note:</b> Unlike Erlang/OTP, IPv6 addresses are not supported.
162+
%% Passing an IPv6 address tuple will return `{error, einval}'.
163+
%% @end
164+
%%-----------------------------------------------------------------------------
165+
-spec ntoa(IpAddress :: ip_address()) -> string() | {error, einval}.
166+
ntoa({A, B, C, D} = Addr) ->
167+
case is_ipv4_address(Addr) of
168+
true ->
169+
integer_to_list(A) ++ "." ++ integer_to_list(B) ++ "." ++ integer_to_list(C) ++ "." ++
170+
integer_to_list(D);
171+
false ->
172+
{error, einval}
173+
end;
174+
ntoa(_) ->
175+
{error, einval}.
176+
177+
%%-----------------------------------------------------------------------------
178+
%% @param Address a string representation of an IPv4 address
179+
%% @returns `{ok, IPv4Address}' if the string is a valid strict IPv4 address,
180+
%% or `{error, einval}' otherwise.
181+
%% @doc Parse an IP address string to an `ip4_address()'.
182+
%%
183+
%% This function is an alias for `parse_ipv4strict_address/1'.
184+
%%
185+
%% <b>Note:</b> Unlike Erlang/OTP, IPv6 addresses are not supported.
186+
%% Only strict dotted-decimal IPv4 addresses with exactly four fields
187+
%% are accepted. Short form addresses such as `"127.1"' or
188+
%% `"0x7f000001"' are not supported. Leading zeros are not supported
189+
%% as well, so `"127.0.0.001"' is not allowed.
190+
%% @end
191+
%%-----------------------------------------------------------------------------
192+
-spec parse_address(Address :: string()) -> {ok, ip4_address()} | {error, einval}.
193+
parse_address(AddrString) ->
194+
parse_ipv4_address(AddrString).
195+
196+
%%-----------------------------------------------------------------------------
197+
%% @param Address a string representation of an IPv4 address
198+
%% @returns `{ok, IPv4Address}' if the string is a valid strict IPv4 address,
199+
%% or `{error, einval}' otherwise.
200+
%% @doc Parse an IPv4 address string to an `ip4_address()'.
201+
%%
202+
%% This function is an alias for `parse_ipv4strict_address/1'.
203+
%%
204+
%% <b>Note:</b> Unlike Erlang/OTP, this function behaves like
205+
%% `parse_ipv4strict_address/1' and only accepts strict dotted-decimal
206+
%% IPv4 addresses with exactly four fields. Short form addresses such
207+
%% as `"127.1"' or `"0x7f000001"' are not supported. Leading zeros are
208+
%% not supported as well, so `"127.0.0.001"' is not allowed.
209+
%% @end
210+
%%-----------------------------------------------------------------------------
211+
-spec parse_ipv4_address(Address :: string()) -> {ok, ip4_address()} | {error, einval}.
212+
parse_ipv4_address(AddrString) ->
213+
parse_ipv4strict_address(AddrString).
214+
215+
%%-----------------------------------------------------------------------------
216+
%% @param Address a string representation of a strict IPv4 address
217+
%% @returns `{ok, IPv4Address}' if the string is a valid strict IPv4 address,
218+
%% or `{error, einval}' otherwise.
219+
%% @doc Parse a strict IPv4 address string to an `ip4_address()'.
220+
%%
221+
%% Requires an IPv4 address string containing exactly four decimal
222+
%% fields separated by dots, where each field is in the range 0-255.
223+
%% For example: `"192.168.0.1"' or `"127.0.0.1"'.
224+
%%
225+
%% Short form addresses such as `"127.1"' or hexadecimal notation
226+
%% such as `"0x7f000001"' are not accepted.
227+
%% @end
228+
%%-----------------------------------------------------------------------------
229+
-spec parse_ipv4strict_address(Address :: string()) -> {ok, ip4_address()} | {error, einval}.
230+
parse_ipv4strict_address(AddrString) ->
231+
case string:split(AddrString, ".", all) of
232+
[A, B, C, D] ->
233+
try
234+
has_no_leading_zeros(A),
235+
has_no_leading_zeros(B),
236+
has_no_leading_zeros(C),
237+
has_no_leading_zeros(D),
238+
MaybeAddr = {
239+
list_to_integer(A), list_to_integer(B), list_to_integer(C), list_to_integer(D)
240+
},
241+
case is_ipv4_address(MaybeAddr) of
242+
true -> {ok, MaybeAddr};
243+
false -> {error, einval}
244+
end
245+
catch
246+
error:badarg -> {error, einval}
247+
end;
248+
_ ->
249+
{error, einval}
250+
end.
251+
252+
has_no_leading_zeros([$0]) ->
253+
ok;
254+
has_no_leading_zeros([H | _T]) when H =/= $0 ->
255+
ok;
256+
has_no_leading_zeros(_) ->
257+
error(badarg).
258+
259+
is_ipv4_address({A, B, C, D}) when
260+
is_integer(A) andalso A >= 0 andalso A < 256 andalso
261+
is_integer(B) andalso B >= 0 andalso B < 256 andalso
262+
is_integer(C) andalso C >= 0 andalso C < 256 andalso
263+
is_integer(D) andalso D >= 0 andalso D < 256
264+
->
265+
true;
266+
is_ipv4_address(_) ->
267+
false.

tests/libs/estdlib/test_inet.erl

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525
test() ->
2626
ok = test_getaddr(),
27+
ok = test_ntoa(),
28+
ok = test_parse_address(),
29+
ok = test_parse_ipv4_address(),
30+
ok = test_parse_ipv4strict_address(),
2731
ok.
2832

2933
test_getaddr() ->
@@ -40,3 +44,80 @@ test_getaddr() ->
4044
{error, einval} = inet:getaddr(<<"localhost">>, inet),
4145
{error, _} = inet:getaddr("localhost.invalid", inet),
4246
ok.
47+
48+
test_ntoa() ->
49+
"127.0.0.1" = inet:ntoa({127, 0, 0, 1}),
50+
"192.168.0.1" = inet:ntoa({192, 168, 0, 1}),
51+
"0.0.0.0" = inet:ntoa({0, 0, 0, 0}),
52+
"255.255.255.255" = inet:ntoa({255, 255, 255, 255}),
53+
case get_otp_version() of
54+
OTPVersion when
55+
(is_integer(OTPVersion) andalso OTPVersion >= 24) orelse OTPVersion == atomvm
56+
->
57+
{error, einval} = inet:ntoa({256, 0, 0, 1}),
58+
{error, einval} = inet:ntoa({0, 0, 0, -1}),
59+
{error, einval} = inet:ntoa({1, 2, 3}),
60+
{error, einval} = inet:ntoa(not_an_address);
61+
_ ->
62+
ok
63+
end,
64+
ok.
65+
66+
get_otp_version() ->
67+
case erlang:system_info(machine) of
68+
"BEAM" -> list_to_integer(erlang:system_info(otp_release));
69+
_ -> atomvm
70+
end.
71+
72+
test_parse_address() ->
73+
{ok, {127, 0, 0, 1}} = inet:parse_address("127.0.0.1"),
74+
{ok, {192, 168, 0, 1}} = inet:parse_address("192.168.0.1"),
75+
{ok, {0, 0, 0, 0}} = inet:parse_address("0.0.0.0"),
76+
{ok, {255, 255, 255, 255}} = inet:parse_address("255.255.255.255"),
77+
{error, einval} = inet:parse_address("256.0.0.1"),
78+
{error, einval} = inet:parse_address("not_an_address"),
79+
case erlang:system_info(machine) of
80+
"BEAM" ->
81+
% BEAM accepts short-form IPv4 addresses
82+
{ok, {127, 0, 0, 1}} = inet:parse_address("127.1"),
83+
{ok, {127, 0, 0, 1}} = inet:parse_address("0x7f000001"),
84+
{ok, {127, 0, 0, 1}} = inet:parse_address("127.0.000.001");
85+
"ATOM" ->
86+
% AtomVM only supports strict dotted-decimal IPv4 addresses
87+
{error, einval} = inet:parse_address("127.1"),
88+
{error, einval} = inet:parse_address("0x7f000001"),
89+
% AtomVM doesn't allow leading zeros as well
90+
{error, einval} = inet:parse_address("127.0.000.001")
91+
end,
92+
ok.
93+
94+
test_parse_ipv4_address() ->
95+
{ok, {127, 0, 0, 1}} = inet:parse_ipv4_address("127.0.0.1"),
96+
{ok, {10, 0, 0, 1}} = inet:parse_ipv4_address("10.0.0.1"),
97+
{ok, {0, 0, 0, 0}} = inet:parse_ipv4_address("0.0.0.0"),
98+
{ok, {255, 255, 255, 255}} = inet:parse_ipv4_address("255.255.255.255"),
99+
{error, einval} = inet:parse_ipv4_address("256.0.0.1"),
100+
{error, einval} = inet:parse_ipv4_address("not_an_address"),
101+
case erlang:system_info(machine) of
102+
"BEAM" ->
103+
% BEAM accepts short-form IPv4 addresses
104+
{ok, {127, 0, 0, 1}} = inet:parse_ipv4_address("127.1"),
105+
{ok, {127, 0, 0, 1}} = inet:parse_ipv4_address("0x7f000001");
106+
"ATOM" ->
107+
% AtomVM only supports strict dotted-decimal IPv4 addresses
108+
{error, einval} = inet:parse_ipv4_address("127.1"),
109+
{error, einval} = inet:parse_ipv4_address("0x7f000001")
110+
end,
111+
ok.
112+
113+
test_parse_ipv4strict_address() ->
114+
{ok, {127, 0, 0, 1}} = inet:parse_ipv4strict_address("127.0.0.1"),
115+
{ok, {10, 0, 0, 1}} = inet:parse_ipv4strict_address("10.0.0.1"),
116+
{ok, {0, 0, 0, 0}} = inet:parse_ipv4strict_address("0.0.0.0"),
117+
{ok, {255, 255, 255, 255}} = inet:parse_ipv4strict_address("255.255.255.255"),
118+
{error, einval} = inet:parse_ipv4strict_address("256.0.0.1"),
119+
{error, einval} = inet:parse_ipv4strict_address("127.0.0"),
120+
{error, einval} = inet:parse_ipv4strict_address("127.1"),
121+
{error, einval} = inet:parse_ipv4strict_address("0x7f000001"),
122+
{error, einval} = inet:parse_ipv4strict_address("not_an_address"),
123+
ok.

0 commit comments

Comments
 (0)