%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
%% Description: This module implements handling of ftp server responses.

-module(ftp_response).
-moduledoc false.

%% Internal API
-export([parse_lines/3, interpret/1, error_string/1]).

-include("ftp_internal.hrl").

%% First group of reply code digits 
-define(POS_PREL, 1).
-define(POS_COMPL, 2).
-define(POS_INTERM, 3).
-define(TRANS_NEG_COMPL, 4).
-define(PERM_NEG_COMPL, 5).
%% Second group of reply code digits
-define(SYNTAX,0).
-define(INFORMATION,1).
-define(CONNECTION,2).
-define(AUTH_ACC,3).
-define(UNSPEC,4).
-define(FILE_SYSTEM,5).

%%%=========================================================================
%%%  INTERNAL API 
%%%=========================================================================

%%--------------------------------------------------------------------------
%% parse_lines(Data, AccLines, StatusCode)  -> {ok, Lines} | 
%%                                             {continue, {Data, 
%%                                                       AccLines, StatusCode}}
%%
%% Data = binary() - data received on the control connection from the 
%%                   ftp-server. 
%% AccLines = [string()] 
%% StatusCode     =  start | {byte(), byte(), byte()} | finish -
%%                   Indicates where in the parsing process we are.
%%                   start - (looking for the status code of the message)
%%                   {byte(), byte(), byte()} - status code found, now
%%                   looking for the last line indication.
%%                   finish - now on the last line.
%% Description: Parses a ftp control response message. 
%%      "A reply is defined to contain the 3-digit code, followed by Space
%%      <SP>, followed by one line of text (where some maximum line length
%%      has been specified), and terminated by the Telnet end-of-line
%%      code (CRLF), or a so called multilined reply for example:
%%
%%                                123-First line
%%                                Second line
%%                                  234 A line beginning with numbers
%%                                123 The last line
%%
%%         The user-process then simply needs to search for the second
%%         occurrence of the same reply code, followed by <SP> (Space), at
%%         the beginning of a line, and ignore all intermediary lines.  If
%%         an intermediary line begins with a 3-digit number, the Server
%%         will pad the front to avoid confusion.
%%--------------------------------------------------------------------------

%% Make sure we received the first 4 bytes so we know how to parse
%% the FTP server response e.i. is the response composed of one
%% or multiple lines.
parse_lines(Bin, Lines, start) when is_binary(Bin), byte_size(Bin) < 4 ->
    {continue, {Bin, Lines, start}};
%% Multiple lines exist
parse_lines(<<C1, C2, C3, $-, Rest/binary>>, Lines, start) ->
    parse_lines(Rest, [$-, C3, C2, C1 | Lines], {C1, C2, C3});
%% Only one line exists
parse_lines(<<C1, C2, C3, ?WHITE_SPACE, Bin/binary>>, Lines, start) ->
    parse_lines(Bin, [?WHITE_SPACE, C3, C2, C1 | Lines], finish);

%% Last line found
parse_lines(<<?CR, ?LF, C1, C2, C3, ?WHITE_SPACE, Rest/binary>>, Lines, {C1, C2, C3}) ->
    parse_lines(Rest, [?WHITE_SPACE, C3, C2, C1, ?LF, ?CR | Lines], finish);
%% Potential end found  wait for more data 
parse_lines(<<?CR, ?LF, C1, C2, C3>> = Bin, Lines, {C1, C2, C3}) ->
    {continue, {Bin, Lines, {C1, C2, C3}}};
%% Intermidate line beginning with status code
parse_lines(<<?CR, ?LF, C1, C2, C3, Rest/binary>>, Lines, {C1, C2, C3}) ->
    parse_lines(Rest, [C3, C2, C1, ?LF, ?CR  | Lines], {C1, C2, C3});

%% Potential last line wait for more data
parse_lines(<<?CR, ?LF, C1, C2>> = Data, Lines, {C1, C2, _} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?CR, ?LF, C1>> = Data, Lines, {C1, _, _} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?CR, ?LF>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?LF>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
%% Part of the multiple lines
parse_lines(<<Octet, Rest/binary>>, Lines, {_,_, _} = StatusCode) ->
    parse_lines(Rest, [Octet | Lines], StatusCode);

%% End of FTP server response found
parse_lines(<<?CR, ?LF>>, Lines, finish) ->
    {ok, lists:reverse([?LF, ?CR | Lines]), <<>>}; 
parse_lines(<<?CR, ?LF, Rest/binary>>, Lines, finish) ->
    {ok, lists:reverse([?LF, ?CR | Lines]), Rest}; 

%% Potential end found  wait for more data 
parse_lines(<<?CR>> = Data, Lines, finish) ->
    {continue, {Data, Lines, finish}};
parse_lines(<<>> = Data, Lines, finish) ->
    {continue, {Data, Lines, finish}};
%% Part of last line
parse_lines(<<Octet, Rest/binary>>, Lines, finish) ->
    parse_lines(Rest, [Octet | Lines], finish).

%%--------------------------------------------------------------------------
%% interpret(Lines) ->  {Status, Text} 
%%	Lines  = [byte(), byte(), byte() | Text] - ftp server response as
%%      returned by parse_lines/3
%%	Stauts = atom() (see interpret_status/3)
%%      Text = [string()]
%%
%% Description: Create nicer data to match on.
%%--------------------------------------------------------------------------
interpret([Didgit1, Didgit2, Didgit3 | Data]) ->
    Code1 = Didgit1 - $0,
    Code2 = Didgit2 - $0,
    Code3 = Didgit3 - $0,
    {interpret_status(Code1, Code2, Code3), Data}.

%%--------------------------------------------------------------------------
%% error_string(Error) -> string()
%%	Error =  {error, term()} | term()
%%
%% Description: Translates error codes into strings intended for
%% human interpretation.
%%--------------------------------------------------------------------------
error_string({error, Reason}) ->
    error_string(Reason);

error_string(echunk) -> "Synchronisation error during chunk sending.";
error_string(eclosed) -> "Session has been closed.";
error_string(econn) ->  "Connection to remote server prematurely closed.";
error_string(eexists) ->"File or directory already exists.";
error_string(ehost) ->  "Host not found, FTP server not found, "
		      "or connection rejected.";
error_string(elogin) -> "User not logged in.";
error_string(enotbinary) -> "Term is not a binary.";
error_string(epath) ->  "No such file or directory, already exists, "
		      "or permission denied.";
error_string(etype) ->  "No such type.";
error_string(euser) ->  "User name or password not valid.";
error_string(etnospc) -> "Insufficient storage space in system.";
error_string(enofile) -> "No files found or file unavailable";
error_string(epnospc) -> "Exceeded storage allocation "
		       "(for current directory or dataset).";
error_string(efnamena) -> "File name not allowed.";
error_string(Reason) -> 
    lists:flatten(io_lib:format("Unknown error: ~w", [Reason])).

%%%========================================================================
%%% Internal functions
%%%========================================================================

%% Positive Preleminary Reply
interpret_status(?POS_PREL,_,_)                   -> pos_prel; 
%%FIXME ??? 3??? interpret_status(?POS_COMPL, ?AUTH_ACC, 3)        -> tls_upgrade; 
interpret_status(?POS_COMPL, ?AUTH_ACC, 4)        -> tls_upgrade; 
%% Positive Completion Reply
interpret_status(?POS_COMPL,_,_)                  -> pos_compl;
%% Positive Intermediate Reply need account
interpret_status(?POS_INTERM,?AUTH_ACC,2)         -> pos_interm_acct;
%% Positive Intermediate Reply
interpret_status(?POS_INTERM,_,_)                 -> pos_interm; 
%% No files found or file not available
interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,0) -> enofile;
%% No storage area no action taken
interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,2) -> etnospc;
%% Temporary Error, no action taken
interpret_status(?TRANS_NEG_COMPL,_,_)            -> trans_neg_compl;
%% Permanent disk space error, the user shall not try again
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,0)  -> epath;
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,2)  -> epnospc;
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,3)  -> efnamena; 
interpret_status(?PERM_NEG_COMPL,?AUTH_ACC,0)     -> elogin; 
interpret_status(?PERM_NEG_COMPL,_,_)             -> perm_neg_compl.

