Current section

Files

Jump to
realpath src realpath.erl
Raw

src/realpath.erl

%% realpath, an Erlang realpath() implementation, by Martin Keegan
%%
%% To the extent (if any) permissible by law, Copyright (C) 2020 Martin Keegan
%%
%% This programme is free software; you may redistribute and/or modify it under
%% the terms of the Apache Software Licence v2.0.
%%%-------------------------------------------------------------------
%%% @author Martin Keegan <martin@no.ucant.org>
%%% @copyright (C) 2020, Martin Keegan
%%% @doc
%%%
%%% @end
%%% Created : 27 May 2020 by Martin Keegan <martin@no.ucant.org>
%%%-------------------------------------------------------------------
-module(realpath).
-include_lib("eunit/include/eunit.hrl").
-export([canonicalise/1]).
-export([normalise/1]).
% This should start with a TTL counter of, say, 20, which should be
% decremented each time a symlink is found, failing if it gets to zero.
% For any path, the string should be broken up into fragments by
% slashes, and each potential initial subset of consecutive fragments
% should be tested to see if its final member is a symlink, i.e.,
%
% for the path /usr/local/man/man1/dwm.1, we should test
% /usr
% /usr/local
% /usr/local/man
% [etc]
%
% If a symlink is encountered, the TTL should be decremented by one and the
% process of substring testing should restart with against the target of
% symlink.
%% @doc
%% Return a canonicalised pathname, having resolved symlinks to their
%% destination. Modelled on realpath(3).
%% @end
-spec canonicalise(string()) -> {'ok', string()} | {'error', atom()}.
canonicalise(Path) when is_list(Path) ->
check_canonical(Path, 20);
canonicalise(_Path) when is_binary(_Path) ->
{error, string_required}.
check_canonical(S, TTL) ->
Fragments = make_fragments(S),
check_fragments(Fragments, [], TTL).
check_fragments(_, _, 0) ->
{error, loop};
check_fragments([], AlreadyChecked, _) ->
{ok, AlreadyChecked};
check_fragments([Head|Tail], AlreadyChecked, TTL) ->
case is_symlink(AlreadyChecked, Head) of
false -> check_fragments(Tail, filename:join(AlreadyChecked, Head),
TTL);
{true, Referent} ->
TailJoined = join_non_null(Tail),
AllJoined = filename:join(Referent, TailJoined),
check_canonical(AllJoined, TTL - 1)
end.
is_symlink(Dirname, Basename) ->
Path = filename:join(Dirname, Basename),
case file:read_link(Path) of
{ok, Name} ->
case Name of
% absolute link
[$/|_] -> {true, Name};
% relative link
_ ->
{true, filename:join(Dirname, Name)}
end;
_ -> false
end.
join_non_null([]) -> "";
join_non_null(SS) -> filename:join(SS).
%% @doc
%% Return a path where the use of ".." to indicate parent directory has
%% been resolved. Currently does not accept relative paths.
%% @end
-spec normalise(list()) -> {'ok', string()} | {'error', atom()}.
normalise(S=[$/|_]) when is_list(S)->
Parts = filename:split(S),
{ok, filename:join(lists:reverse(normalise(Parts, [])))};
normalise(_S) when is_list(_S) ->
{error, relative_path};
normalise(_) ->
{error, string_required}.
normalise([], Path) -> Path;
normalise([".."|T], Path) ->
{_H, Rest} = pop(Path),
normalise(T, Rest);
normalise([H|T], Path) ->
Rest = push(H, Path),
normalise(T, Rest).
pop([]) -> {"/", []};
pop(["/"]) -> {"/", ["/"]};
pop([H|T]) -> {H,T}.
push(H,T) -> [H|T].
make_fragments(S) ->
filename:split(S).
make_fragments_test_data() ->
[{"/usr/local/bin", ["/", "usr", "local", "bin"]},
{"usr/local/bin/bash", ["usr", "local", "bin", "bash"]},
{"/usr/local/bin/", ["/", "usr", "local", "bin"]}].
make_fragments_test_() ->
[ ?_assertEqual(Expected, make_fragments(Observed))
|| {Observed, Expected} <- make_fragments_test_data() ].