(**************************************************************************)
(*                                                                        *)
(*  Support modules for network applications                              *)
(*  Copyright (C) 2025   Peter Moylan                                     *)
(*                                                                        *)
(*  This program is free software: you can redistribute it and/or modify  *)
(*  it under the terms of the GNU General Public License as published by  *)
(*  the Free Software Foundation, either version 3 of the License, or     *)
(*  (at your option) any later version.                                   *)
(*                                                                        *)
(*  This program is distributed in the hope that it will be useful,       *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
(*  GNU General Public License for more details.                          *)
(*                                                                        *)
(*  You should have received a copy of the GNU General Public License     *)
(*  along with this program.  If not, see <http://www.gnu.org/licenses/>. *)
(*                                                                        *)
(*  To contact author:   http://www.pmoylan.org   peter@pmoylan.org       *)
(*                                                                        *)
(**************************************************************************)

IMPLEMENTATION MODULE SBuffers;

        (********************************************************)
        (*                                                      *)
        (*       Buffers for line-oriented socket input         *)
        (*                                                      *)
        (*  Programmer:         P. Moylan                       *)
        (*  Started:            24 May 1998                     *)
        (*  Last edited:        21 September 2025               *)
        (*  Status:             OK                              *)
        (*                                                      *)
        (*   Now supports TLS                                   *)
        (*                                                      *)
        (********************************************************)


IMPORT TLS;

FROM SYSTEM IMPORT
    (* type *)  LOC, ADDRESS,
    (* proc *)  ADR;

FROM Sockets IMPORT
    (* const*)  NotASocket,
    (* type *)  Socket,
    (* proc *)  send, recv, soclose;

FROM TLS IMPORT
    (* type *)  TLSsession,
    (* proc *)  OpenTLSsession, CloseTLSsession, TLSsend,
                TLSrecv, TLSrecvLine;

FROM TransLog IMPORT
    (* type *)  TransactionLogID,
    (* proc *)  LogTransactionL;

FROM LowLevel IMPORT
    (* proc *)  EVAL, Copy, AddOffset;

IMPORT Inet2Misc, MiscFuncs, Strings;

FROM Storage IMPORT
    (* proc *)  ALLOCATE, DEALLOCATE;

(************************************************************************)

CONST
    Nul = CHR(0); CR = CHR(13); LF = CHR(10); CtrlZ = CHR(26);
    InBufferSize = 16384;
    OutBufferSize = 16384;
    LineBufferSize = 4096;

TYPE
    InputBufferSubscript = [0..InBufferSize-1];
    LineBufferSubscript = [0..LineBufferSize-1];
    OutBufferSubscript = [0..OutBufferSize-1];

    (* InputBuffer is a buffer to hold incoming data.  RBpos is the     *)
    (* character position we're up to in InputBuffer, and RBlength is   *)
    (* the number of characters in InputBuffer, including the ones we   *)
    (* have already processed.  (That is, the number of InputBuffer     *)
    (* characters still to be processed is RBlength-RBpos.)             *)
    (* LineBuffer is a copy of the incoming line.  For line-oriented    *)
    (* input we copy from InputBuffer to LineBuffer.                    *)
    (* Timeout is the time in milliseconds before we decide that a      *)
    (* connection for incoming data has been lost.  We make this        *)
    (* variable so that we can use a relatively short value when making *)
    (* the initial connection, and then increase the value once the     *)
    (* connection has been established.                                 *)
    (* OutBuffer is a buffer to hold outgoing data, and OutAmount is a  *)
    (* count of how many characters are now in OutBuffer.               *)
    (* OutputFailed says that an error occurred on output.              *)

    SBuffer = POINTER TO SBrecord;
    SBrecord = RECORD
                   socket: Socket;
                   TLShandle: TLSsession;
                   logID: TransactionLogID;
                   InputBuffer:
                          ARRAY InputBufferSubscript OF CHAR;
                   RBpos, RBlength: CARDINAL;
                   Timeout: CARDINAL;
                   LineBuffer:
                          ARRAY LineBufferSubscript OF CHAR;
                   OutBuffer:
                          ARRAY OutBufferSubscript OF CHAR;
                   OutAmount: CARDINAL;
                   PlusMinusResponse: BOOLEAN;
                   OutputFailed: BOOLEAN;
                   server: BOOLEAN;
                   TLS: BOOLEAN;
               END (*RECORD*);

VAR
    CRLF: ARRAY [0..1] OF CHAR;
    LineFeed: ARRAY [0..0] OF CHAR;

(************************************************************************)

PROCEDURE SetSingleCertificateMode;

    (* Sets a mode where the domain name is ignored when fetching a     *)
    (* server certificate or private key, because the one certificate   *)
    (* is used for all domains.                                         *)

    BEGIN
        TLS.SetSingleCertificateMode;
    END SetSingleCertificateMode;

(************************************************************************)
(*                CREATING AND DESTROYING AN SBuffer                    *)
(************************************************************************)

PROCEDURE CreateSBuffer (s: Socket;  IsServer, useTLS, NumericReplyCode: BOOLEAN;
                            serveraddr: CARDINAL;
                            serverdomain: ARRAY OF CHAR;
                                logID: TransactionLogID): SBuffer;

    (* Creates a new SBuffer.  We assume that the connection on socket  *)
    (* s has already been established by the caller.  The               *)
    (* NumericReplyCode parameter controls how response codes from the  *)
    (* peer are encoded.  If this parameter is FALSE, we expect a '+'   *)
    (* or '-' status reply code.  If it is TRUE, we expect a            *)
    (* three-digit numeric code.                                        *)
    (* If we are acting as a client, then serveraddr and serverdomain   *)
    (* specify the peer.  If we are acting as a server, serveraddr      *)
    (* should be zero and serverdomain an empty string.                 *)

    VAR result: SBuffer;

    BEGIN
        NEW (result);
        IF result <> NIL THEN
            result^.logID := logID;
            WITH result^ DO
                socket := s;
                server := IsServer;
                RBpos := 0;  RBlength := 0;
                OutAmount := 0;
                Timeout := 75000;
                PlusMinusResponse := NOT NumericReplyCode;
                TLShandle := NIL;
                OutputFailed := FALSE;
                TLS := useTLS;
            END (*WITH*);
            IF result^.TLS THEN
                IF OpenTLSsession(s, IsServer, serveraddr,
                          serverdomain, logID, result^.TLShandle) THEN
                ELSE
                    LogTransactionL (logID, "TLS handshaking failed");
                    DISPOSE (result);
                END (*IF*);
            END (*IF*);
        END (*IF*);
        RETURN result;
    END CreateSBuffer;

(************************************************************************)

PROCEDURE StartTLS (SB: SBuffer;  serveraddr: CARDINAL;
                                serverdomain: ARRAY OF CHAR): BOOLEAN;

    (* Switches from plain mode to TLS mode.  Assumption: the stream    *)
    (* has already been opened with TLS = FALSE.  There is no way of    *)
    (* switching out of this mode again.                                *)

    BEGIN
        IF (SB = NIL) OR SB^.TLS THEN
            (* Operation fails - illegal call. *)
            RETURN FALSE;
        ELSE
            WITH SB^ DO
                TLS := OpenTLSsession(socket, server, serveraddr,
                                        serverdomain, logID, TLShandle);
                IF TLS THEN
                    RETURN TRUE;
                ELSE
                    LogTransactionL (logID, "StartTLS failed");
                    RETURN FALSE;
                END (*IF*);
            END (*WITH*);
        END (*IF*);
    END StartTLS;

(************************************************************************)

PROCEDURE TLSactive (SB: SBuffer): BOOLEAN;

    (* Returns TRUE iff this is an encrypted channel. *)

    BEGIN
        RETURN SB^.TLS;
    END TLSactive;

(************************************************************************)

PROCEDURE CloseSBuffer (VAR (*INOUT*) SB: SBuffer);

    (* Releases the buffer space, closes the socket. *)

    BEGIN
        IF SB <> NIL THEN
            IF SB^.TLS THEN
                CloseTLSsession (SB^.TLShandle);
            END (*IF*);
            IF SB^.socket <> NotASocket THEN
                EVAL (soclose(SB^.socket));
            END (*IF*);
            DEALLOCATE (SB, SIZE(SBrecord));
        END (*IF*);
    END CloseSBuffer;

(************************************************************************)

PROCEDURE IsClosed (SB: SBuffer): BOOLEAN;

    (* Returns TRUE if this buffer is no longer available. *)

    BEGIN
        RETURN SB = NIL;
    END IsClosed;

(************************************************************************)

PROCEDURE SetTimeout (SB: SBuffer;  seconds: CARDINAL);

    (* Sets the timeout value to the given number of seconds. *)

    BEGIN
        SB^.Timeout := 1000*seconds;
    END SetTimeout;

(************************************************************************)

PROCEDURE SocketOf (SB: SBuffer): Socket;

    (* Returns the socket belonging to this SBuffer. *)

    BEGIN
        IF SB = NIL THEN
            RETURN NotASocket;
        ELSE
            RETURN SB^.socket;
        END (*IF*);
    END SocketOf;

(************************************************************************)
(*                                 OUTPUT                               *)
(************************************************************************)

PROCEDURE PutBytes (SB: SBuffer;  VAR (*IN*) message: ARRAY OF LOC;
                                                length: CARDINAL): CARDINAL;

    (* Sends a message of "length" bytes.  The returned value is the number *)
    (* of bytes sent, or MAX(CARDINAL) if there was an error.               *)

    BEGIN
        IF SB^.TLS THEN
            IF TLSsend (SB^.TLShandle, message, length) THEN
                RETURN length;
            ELSE
                RETURN MAX(CARDINAL);
            END (*IF*);
        ELSE
            RETURN send(SB^.socket, message, length, 0);
        END (*IF*);
    END PutBytes;

(************************************************************************)

PROCEDURE FlushOutput (SB: SBuffer): CARDINAL;

    (* Sends out any remaining buffered output.  Returns the actual     *)
    (* number of bytes sent.  Sets SB^.OutputFailed on failure.         *)

    VAR amountsent: CARDINAL;

    BEGIN
        amountsent := 0;
        IF SB <> NIL THEN
            WITH SB^ DO
                IF OutAmount > 0 THEN
                    amountsent := PutBytes (SB, OutBuffer, OutAmount);
                    IF amountsent = MAX(CARDINAL) THEN
                        amountsent := 0;
                        OutputFailed := TRUE;
                    END (*IF*);
                    OutAmount := 0;
                    Inet2Misc.Synch (socket);
                END (*IF*);
            END (*WITH*);
        END (*IF*);
        RETURN amountsent;
    END FlushOutput;

(************************************************************************)

PROCEDURE AddToBuffer (SB: SBuffer;  VAR (*IN*) data: ARRAY OF LOC;
                                  amount: CARDINAL;
                                  VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Puts 'amount' characters into the output buffer.  Output value   *)
    (* 'sent' gives the number of characters actually sent.  This could *)
    (* be less than amount if there was a network problem, or if some   *)
    (* of the data are still sitting in SB^.OutBuffer at the end of     *)
    (* the operation (and still available to be sent out by calling     *)
    (* FlushOutput).                                                    *)

    VAR place, count: CARDINAL;

    BEGIN
        sent := 0;
        IF SB = NIL THEN
            RETURN FALSE;
        END (*IF*);
        place := 0;
        WHILE amount > 0 DO
            count := OutBufferSize - SB^.OutAmount;
            IF count <= amount THEN
                Copy (ADR(data[place]),
                        ADR(SB^.OutBuffer[SB^.OutAmount]),
                                        count);
                INC (place, count);
                DEC (amount, count);
                INC (SB^.OutAmount, count);
                INC (sent, FlushOutput (SB));
            ELSE
                Copy (ADR(data[place]), ADR(SB^.OutBuffer[SB^.OutAmount]),
                                        amount);
                INC (SB^.OutAmount, amount);
                amount := 0;
            END (*IF*);
        END (*WHILE*);
        RETURN NOT SB^.OutputFailed;
    END AddToBuffer;

(************************************************************************)

PROCEDURE SendLine (SB: SBuffer;  VAR (*IN*) line: ARRAY OF CHAR;
                                VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Sends the string, appending a CRLF. *)

    VAR sent1, sent2: CARDINAL;  success: BOOLEAN;

    BEGIN
        sent2 := 0;
        success := AddToBuffer(SB, line, LENGTH(line), sent1)
                          AND AddToBuffer(SB, CRLF, 2, sent2);
        sent := sent1 + sent2;
        IF success THEN
            INC (sent, FlushOutput (SB));
        END (*IF*);
        RETURN success;
    END SendLine;

(************************************************************************)

PROCEDURE SendLineL (SB: SBuffer;  line: ARRAY OF CHAR;
                                VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Like SendLine, but for a literal string. *)

    BEGIN
        RETURN SendLine (SB, line, sent);
    END SendLineL;

(************************************************************************)

PROCEDURE SendString (SB: SBuffer;  line: ARRAY OF CHAR;
                                VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Sends the string, without appending a CRLF. *)

    BEGIN
        RETURN AddToBuffer(SB, line, LENGTH(line), sent);
    END SendString;

(************************************************************************)

PROCEDURE SendRaw (SB: SBuffer;  VAR (*IN*) data: ARRAY OF LOC;
                  amount: CARDINAL;  VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Sends uninterpreted data.  Output parameter 'sent' says how      *)
    (* many bytes were actually sent.                                   *)

    VAR success: BOOLEAN;

    BEGIN
        success := AddToBuffer(SB, data, amount, sent);
        IF success THEN
            INC (sent, FlushOutput (SB));
        END (*IF*);
        RETURN success;
    END SendRaw;

(************************************************************************)

PROCEDURE SendChar (SB: SBuffer;  ch: CHAR;
                             VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Sends a single character. *)

    VAR buffer: ARRAY [0..0] OF CHAR;

    BEGIN
        buffer[0] := ch;
        RETURN AddToBuffer(SB, buffer, 1, sent);
    END SendChar;

(************************************************************************)

PROCEDURE SendEOL (SB: SBuffer;  VAR (*OUT*) sent: CARDINAL): BOOLEAN;

    (* Sends a CRLF. *)

    VAR result: BOOLEAN;

    BEGIN
        result := AddToBuffer(SB, CRLF, 2, sent);
        INC (sent, FlushOutput (SB));
        RETURN result;
    END SendEOL;

(************************************************************************)
(*                                INPUT                                 *)
(************************************************************************)

PROCEDURE LoadInputBuffer (SB: SBuffer;  wanted: CARDINAL);

    (* Loads up to wanted bytes into SB^.InputBuffer   *)

    VAR actual: CARDINAL;  ConnectionLost: BOOLEAN;

    BEGIN
        IF wanted > InBufferSize THEN
            wanted := InBufferSize;
        END (*IF*);
        IF SB^.TLS THEN
            TLSrecv (SB^.TLShandle, SB^.InputBuffer,
                            wanted, actual, ConnectionLost);
            IF ConnectionLost THEN
                actual := MAX(CARDINAL);
            END (*IF*);
            SB^.RBlength := actual;
        ELSE
            (* WaitForSocket returns +1 for OK, 0 fortimeout,   *)
            (* and -1 for error.                                *)

            IF Inet2Misc.WaitForSocket (SB^.socket, SB^.Timeout) > 0 THEN
                SB^.RBlength := recv (SB^.socket, SB^.InputBuffer, wanted, 0);
            ELSE
                SB^.RBlength := MAX(CARDINAL);
            END (*IF*);
        END (*IF*);
    END LoadInputBuffer;

(************************************************************************)

PROCEDURE Getch (SB: SBuffer): CHAR;

    (* Result is Nul if connection fails. *)
    (* Assumption: SB <> NIL. *)

    VAR result: CHAR;  EmptyResponseCount: CARDINAL;

    BEGIN
        WITH SB^ DO
            IF RBpos >= RBlength THEN
                RBpos := 0;
                EmptyResponseCount := 0;
                REPEAT
                    LoadInputBuffer (SB, InBufferSize);
                    IF RBlength = 0 THEN
                        INC (EmptyResponseCount);
                        IF EmptyResponseCount > 20 THEN
                            RBlength := MAX(CARDINAL);
                        END (*IF*);
                    END (*IF*);
                UNTIL RBlength <> 0;
                IF RBlength = MAX(CARDINAL) THEN
                    RBlength := 0;
                    RETURN Nul;
                END (*IF*);
            END (*IF*);
            result := InputBuffer[RBpos];  INC(RBpos);
        END (*WITH*);
        RETURN result;
    END Getch;

(************************************************************************)

PROCEDURE GetBlock (SB: SBuffer;  wanted: CARDINAL;
                                  p: MiscFuncs.LocArrayPointer): CARDINAL;

    (* Gets at most "wanted" bytes of raw data, returns the number of   *)
    (* bytes actually fetched.  A function return of MAX(CARDINAL)      *)
    (* means that the connection failed.                                *)

    VAR amount: CARDINAL;
        source: ADDRESS;

    BEGIN
        WITH SB^ DO

            (* InputBuffer holds RBlength-RBpos unused characters.  *)

            IF RBpos >= RBlength THEN

                (* No data available, so reload input buffer. *)

                RBpos := 0;
                LoadInputBuffer (SB, wanted);
            END (*IF*);

            IF RBlength = MAX(CARDINAL) THEN
                amount := MAX(CARDINAL);
                RBlength := 0;
            ELSE
                amount := RBlength - RBpos;
                IF amount > wanted THEN
                    amount := wanted;
                END (*IF*);
                IF amount > 0 THEN
                    source := ADR(InputBuffer[RBpos]);
                    Copy (source, p, amount);
                    INC (RBpos, amount);
                END (*IF*);
            END (*IF*);

        END (*WITH*);

        RETURN amount;

    END GetBlock;

(************************************************************************)

PROCEDURE GetRaw (SB: SBuffer;  size: CARDINAL;
                                p: MiscFuncs.LocArrayPointer): BOOLEAN;

    (* Fetches a block of exactly size uninterpreted bytes, stores the  *)
    (* result at p^.  Returns FALSE if the operation failed.            *)

    VAR wanted, amount, EmptyResponseCount: CARDINAL;

    BEGIN
        EmptyResponseCount := 0;
        wanted := size;
        WHILE wanted > 0 DO
            amount := GetBlock (SB, wanted, p);
            IF amount = 0 THEN
                INC (EmptyResponseCount);
                IF EmptyResponseCount > 20 THEN
                    RETURN FALSE;
                END (*IF*);
            ELSIF amount = MAX(CARDINAL) THEN
                RETURN FALSE;
            ELSIF amount > 0 THEN
                p := AddOffset (p, amount);
                DEC (wanted, amount);
            END (*IF*);
        END (*WHILE*);
        RETURN TRUE;
    END GetRaw;

(************************************************************************)

PROCEDURE LoadLineBuffer (SB: SBuffer): BOOLEAN;

    (* Loads the next incoming line of text into SB^.LineBuffer.        *)
    (* Assumption: a line ends with CRLF.  To avoid tortuous logic, I   *)
    (* take the LF as end of line and skip the CR.  At end of input we  *)
    (* return with line[0] = Ctrl/Z.                                    *)
    (* A function return of FALSE means that the connection failed or   *)
    (* we have reached end of input.                                    *)

    VAR length, pos, extra, space, LBpos: CARDINAL;
        found: BOOLEAN;
        source, destination: ADDRESS;

    BEGIN
        space := MAX(LineBufferSubscript) + 1;
        LBpos := 0;
        destination := ADR(SB^.LineBuffer);
        REPEAT
            found := FALSE;
            WITH SB^ DO

                (* InputBuffer holds RBlength-RBpos unused characters.  *)
                (* LineBuffer has room for 'space' more characters.     *)

                IF RBpos >= RBlength THEN
                    RBpos := 0;
                    LoadInputBuffer (SB, InBufferSize);
                    IF (RBlength = 0) OR (RBlength = MAX(CARDINAL)) THEN
                        RBlength := 0;
                        IF space > 0 THEN
                            LineBuffer[LBpos] := CtrlZ;
                            INC (LBpos);
                            destination := AddOffset (destination, 1);
                            DEC (space);
                        END (*IF*);
                        found := TRUE;
                    END (*IF*);
                END (*IF*);

                IF NOT found THEN
                    source := ADR(InputBuffer[RBpos]);
                    IF RBlength < InBufferSize THEN
                        InputBuffer[RBlength] := Nul;
                    END (*IF*);
                    Strings.FindNext (LineFeed, InputBuffer, RBpos, found, pos);
                    IF found AND (pos < RBlength) THEN
                        extra := 1;
                    ELSE
                        pos := RBlength;
                        extra := 0;
                    END (*IF*);
                    length := pos - RBpos;

                    (* errinfo says invalid index at this point. *)

                    IF (length > 0) AND (InputBuffer[pos-1] = CR) THEN
                        DEC (length);  INC (extra);
                    END (*IF*);

                    (* At this point length is the number of characters *)
                    (* up to but not including the CRLF.  The variable  *)
                    (* extra counts the number of bytes, if any, in the *)
                    (* line terminator.                                 *)

                    IF length > space THEN
                        INC (extra, length-space);
                        length := space;
                    END (*IF*);

                    (* By now it's possible that extra includes some    *)
                    (* characters that aren't line terminators but      *)
                    (* which can't fit in the line buffer.              *)

                    IF length > 0 THEN
                        Copy (source, destination, length);
                        DEC (space, length);
                        destination := AddOffset (destination, length);
                        INC (LBpos, length);
                    END (*IF*);
                    INC (length, extra);
                    IF found THEN
                        IF length > 0 THEN

                            (* Skip to end of line in the receive       *)
                            (* buffer, even if we've had to truncate    *)
                            (* the line in LineBuffer.                  *)

                            INC (RBpos, length);
                        END (*IF*);
                    ELSE
                        (* Line terminator not yet found, force a       *)
                        (* further read from the input.                 *)

                        RBlength := 0;
                    END (*IF*);
                END (*IF*);
            END (*WITH*);
        UNTIL found OR (space = 0);

        IF space > 0 THEN
            SB^.LineBuffer[LBpos] := Nul;
        END (*IF*);

        RETURN SB^.LineBuffer[0] <> CtrlZ;

    END LoadLineBuffer;

(************************************************************************)

PROCEDURE GetLine (SB: SBuffer;  VAR (*OUT*) line: ARRAY OF CHAR): BOOLEAN;

    (* Reads a line of text from a file.  Assumption: a line ends with  *)
    (* CRLF.  To avoid tortuous logic, I take the LF as end of line and *)
    (* skip the CR.  At end of file we return with line[0] = Ctrl/Z.    *)
    (* A function return of FALSE means that the connection failed.     *)

    VAR success: BOOLEAN;

    BEGIN
        success := LoadLineBuffer (SB);
        Strings.Assign (SB^.LineBuffer, line);
        RETURN success;
    END GetLine;

(************************************************************************)

PROCEDURE GetResponse (SB: SBuffer;  VAR (*OUT*) MoreToCome: BOOLEAN): BOOLEAN;

    (* Returns one line of the server response to a command.            *)
    (* MoreToCome is set if this is part of a multi-line response, and  *)
    (* it's not the last line.   The function result is FALSE if we've  *)
    (* lost the connection.                                             *)
    (* Assumption: SB <> NIL. *)

    TYPE CharSet = SET OF CHAR;

    CONST Digits = CharSet {'0'..'9'};

    VAR success: BOOLEAN;

    BEGIN
        success := LoadLineBuffer (SB);
        MoreToCome := NOT(SB^.LineBuffer[0] IN Digits)
                         OR
                         ((SB^.LineBuffer[1] IN Digits)
                          AND (SB^.LineBuffer[2] IN Digits)
                          AND (SB^.LineBuffer[3] = '-'));
        RETURN success;
    END GetResponse;

(************************************************************************)

PROCEDURE ResponseCode (SB: SBuffer): CARDINAL;

    (* Receives a (possibly multi-line) response from the server, and   *)
    (* returns the first digit of the numeric code.  The values are:    *)
    (*      0  Connection lost                                          *)
    (*      1  OK, another reply still to come                          *)
    (*      2  OK, command done                                         *)
    (*      3  OK, another command expected                             *)
    (*      4  Transient failure, try again later                       *)
    (*      5  Definite failure                                         *)
    (*      6  Reply code is not numeric                                *)
    (*      7  Connection lost or SB=NIL                                *)

    TYPE CharSet = SET OF CHAR;
    CONST Digits = CharSet {'0'..'9'};

    VAR active, MoreToCome: BOOLEAN;

    BEGIN
        IF SB = NIL THEN
            RETURN 7;
        END (*IF*);
        REPEAT
            active := GetResponse (SB, MoreToCome);
        UNTIL SB^.PlusMinusResponse OR NOT (MoreToCome AND active);
        IF active THEN
            IF SB^.PlusMinusResponse THEN
                IF SB^.LineBuffer[0] = '+' THEN
                    RETURN 2;
                ELSE
                    RETURN 5;
                END (*IF*);
            ELSIF SB^.LineBuffer[0] IN Digits THEN
                RETURN ORD(SB^.LineBuffer[0]) - ORD('0');
            ELSE
                RETURN 6;
            END (*IF*);
        ELSE
            RETURN 7;
        END (*IF*);
    END ResponseCode;

(************************************************************************)

PROCEDURE PositiveResponse (SB: SBuffer;
                            VAR (*OUT*) LostConnection: BOOLEAN): BOOLEAN;

    (* Returns TRUE if a positive response was returned.  *)

    TYPE ReplySet = SET OF [0..7];

    VAR code: [0..7];

    BEGIN
        code := ResponseCode(SB);
        LostConnection := code IN ReplySet{0, 7};
        RETURN code IN ReplySet{1..3};
    END PositiveResponse;

(************************************************************************)

PROCEDURE GetLastLine (SB: SBuffer;  VAR (*OUT*) line: ARRAY OF CHAR);

    (* Returns a copy of the last line received. *)

    BEGIN
        IF SB = NIL THEN
            line[0] := Nul;
        ELSE
            Strings.Assign (SB^.LineBuffer, line);
        END (*IF*);
    END GetLastLine;

(************************************************************************)

BEGIN
    CRLF[0] := CR;
    CRLF[1] := LF;
    LineFeed[0] := LF;
END SBuffers.

