!fnescape.bsi, 1.0(102)  ! various escaping/unescaping functions
!------------------------------------------------------------------------
!EDIT HISTORY
!Version 1.0:-
! [100] 04-Nov-19 / jdm / created
! [101] 25-Apr-26 / jdm / add Fn'Excape$()
! [102] 28-Apr-26 / jdm / tweak URI mode for special handling of " " <-> "+" 
! [103] 28-Apr-26 / jdm / add test code 
!------------------------------------------------------------------------
!REQUIREMENTS
!
!NOTES
!   Warning: the FNESCF_URI mode should only be used on URI-style strings, due 
!   to the conditional conversion of spaces to "+" only when following a "?"
!   earlier in the string. (This relates to a web standard of encoding
!   the query parameters for an HTTP GET as a suffix on the web address
!   (xxx.com?query...) in which case spaces may be encoded as "+".) But
!   if used with more general text, the conditional unescaping of "+"
!   may result in literal "+" being converted to space.
!
!   As a further complication, CGIUTL CGIOP_UNESCAPE prior to 7.0.1785.7
!   converted all "+" back to space; after that it only performs that 
!   conversion if there is a "?" earlier in the string. To match this
!   behavior in Fn'Escape(), we check the version.
!
!   For general multi-line text, either the default FNESCF_PCT_HEX or 
!   FNESCF_CARET modes allow for any text to be escaped and then unescaped
!   without changes.
!------------------------------------------------------------------------
!Keywords: string escape uri encoding percent hex caret
!------------------------------------------------------------------------
!PUBLIC FUNCTIONS
!   Fn'Escape$(s) - escape 'problem chars' in s using URI or 'percent escaping'
!   Fn'UnEscape$(s) - unescape an URI-style escaped or percent-escaped string
!------------------------------------------------------------------------

++include'once ashinc:ashell.def
++include'once ashinc:types.def         ! [102]
++include'once sosfunc:fndec2hex.bsi    ! [101] 
++include'once sosfunc:fnminasver.bsi   ! [102] 

define FNESCF_PCT_HEX  = &h0001         ! %xx (default)
define FNESCF_URI      = &h0002         ! include all URI reserved chars (space, !, #, $, etc.; else 1-31 and 129-255)
define FNESCF_CARET    = &h0004         ! use caret-style (^M, ^C etc.)

define URI_RSV_CHARS$ = "!#$&'()*+,/:;=?@[] "    ! URI-reserved chars (see note about space above)

map1 VERSYS_FNESCAPE_BSI,s,40,">>@VERSYS(1)->>fnescape.bsi[103]"        ! [103] 

!======================================================================
! [103] Simple test routine; conditionally compile and run via:
!   .COMPIL FNESCAPE.BSI/X:2/PX/C:FNESCAPE_BSI_TEST=1 
!   .RUN FNESCAPE
!======================================================================
++ifdef FNESCAPE_BSI_TEST

    ++message Compiling FNESCAPE.BSI test routine...
    
    map1 testvars
        map2 op,b,1
        map2 sin$,s,0
        map2 sout$,s,0
        map2 infile$,T_NATIVEPATH
        map2 outfile$,T_NATIVEPATH
        map2 flags,b,4
        map2 rcvd,b,2
        map2 c,s,1
        map2 outch,b,2
        
    ? "Test Fn'Escape() and Fn'Unescape() in fnescape.bsi"
    ?
    do
        input "1) Fn'Escape(), 2) Fn'Unescape, 0) quit: ", op
    
        if op = 0 exit
        on op call ESCAPE, UNESCAPE
    loop 
    end
    
ESCAPE:
    input line "Input text (or file) to escape: ",sin$
    input line "Output file (blank for screen): ",outfile$
    input "Flags (1=FNESCF_PCT_HEX, 2=FNESCF_URI, 4=FNESCF_CARET): ",flags
    
    if lookup(sin$) then
        open #1, sin$, input
        sin$ = ""
        do while eof(1)=0
            xcall GET,c,1,1,rcvd        ! input 1 byte at a time
            if rcvd = 1 then
                sin$ += c
            else
                exit
            endif
        loop
        close #1
    endif
    
    if outfile$ # "" then
        outch = 2
        open #outch, outfile$, output
    endif
    
    if outch = 0 then
        ? "Escaped text: ";
    endif
    ? #outch, Fn'Escape$(sin$,flags)
    
    close #outch
    if outch # 0 then
        xcall EZTYP, outfile$
    endif
    return
    
UNESCAPE:    
    input line "Input text (or file) to unescape: ",sin$
    input line "Output file: ",outfile$
    input "Flags (1=FNESCF_PCT_HEX, 2=FNESCF_URI, 4=FNESCF_CARET): ",flags
    
    if lookup(sin$) then
        open #1, sin$, input
        sin$ = ""
        do while eof(1)=0
            xcall GET,c,1,1,rcvd        ! input 1 byte at a time
            if rcvd = 1 then
                sin$ += c
            else
                exit
            endif
        loop
        close #1
    endif

    if outfile$ # "" then
        outch = 2
        open #outch, outfile$, output
    else
        ? "Unescaped text: ";
    endif
    ? #outch, Fn'Unescape$(sin$,flags)

    close #outch
    if outch then
        xcall EZTYP, outfile$
    endif
    return   
 
++endif         ! end of test routine

!---------------------------------------------------------------------
!Function:
!   Convert problem chars in string using %xx or ^x escaping
!Params:
!   s  (str) [in] - string to escape
!   flags (num) [in] - FNESCF_xxx
!Returns:
!   escaped string
!Globals:
!Notes:
!---------------------------------------------------------------------
Function Fn'Escape$(s as s0:inputonly, flags=FNESCF_PCT_HEX as b4:inputonly) as s0
    
    map1 locals
        map2 i,i,4
        map2 c,s,1
        map2 bval,b,1
        map2 bquery,BOOLEAN         ! [102] set when we see "?"
        map2 v17857,i,2             ! [102] A-Shell 7.0.1785.7+
    
    
    if flags and FNESCF_URI then    ! [102] check A-Shell version for compatible handling of +
        v17857 = Fn'MinAshVer(vmajor=7, vminor=0,vedit=1785, vpatch=7)  ! v17857 >= 0 if version >= 7.0.1785.7
    endif
    
    trace.print (99,"esc") ABC_CURRENT_ROUTINE$, flags    
    do
        i += 1
        c = s[i;1]
        bval = asc(c)
        trace.print (99,"esc") i,c,bval
        if bval then
            if bval < 32 then                                       ! ctl chars -> %xx or %x
                if (flags and FNESCF_CARET) then
                    .fn += "^" + chr(bval+asc("A")-1)
                else
                    .fn += "%" + Fn'Dec2Hex$(decval=bval,width=2)
                endif
                
            elseif bval > 127 then                                  ! 8 bit chars -> %xx
               .fn += "%" + Fn'Dec2Hex$(decval=bval,width=2)
               
            elseif instr(1,URI_RSV_CHARS$,c)                        ! URI reserved chars ...
                if (flags and FNESCF_URI) then                      ! if URI option then -> %xx
                    if (c = " ") then                               ! [102] special handling for space
                        if bquery or (v17857 < 0) then              ! [102] convert to + if after a ? (or older version)
                            .fn += "+"                              ! [102] then convert to "+"
                        else                                        ! [102]
                            .fn = c                                 ! [102] else leave as space
                        endif
                    else                                            ! [101] else to %xx
                        .fn += "%" + Fn'Dec2Hex$(decval=bval,width=2)
                    endif
                    if c = "?" then                                 ! [102] presence of ? affects
                        bquery = .TRUE                              ! [102] subsequent handling of space 
                    endif   
                else
                    .fn += c                                        ! else literal
                endif
                
            elseif c = "%" then                                     ! %
                if (flags and FNESCF_CARET) = 0  then               ! if not ^ mode
                    .fn += "%25"                                    ! -> %25
                else
                    .fn += c                                        ! else literal
                endif
                
            else
                .fn += c                                            ! all else literal
            endif
        else
            exit
        endif
    loop

EndFunction

!---------------------------------------------------------------------
!Function:
!   Unescape a string containing %xx and/or ^x escaped chars
!Params:
!   s (str) [in] - string to unescape
!   flags (num) [in] - indicate types of unescaping to do
!       FNESCF_PCT_HEX  - %xx
!       FNESCF_CARET - 
!Returns:
!Globals:
!Notes:
!   The CGIOP_UNESCAPE logic prior to 7.0.1785.6 converts all "+" to space;
!   after that, it only does so if their is a "?" prior to that in the string.
!   (This matches the Fn'Escape() logic above.)
!---------------------------------------------------------------------
Function Fn'Unescape$(s as s0:inputonly, flags=FNESCF_PCT_HEX as b4:inputonly) as s0

    map1 locals
        map2 status,i,4
        map2 i,i,4
        map2 c,s,1
        map2 bval,b,1
        
    trace.print (99,"esc") ABC_CURRENT_ROUTINE$, flags    
    if (flags and FNESCF_CARET) = 0 then                ! [101] FNESCF_PCT_HEX or _URI) then
        xcall CGIUTL, CGIOP_UNESCAPE, s, .fn, status    ! std URI style unescaping
        trace.print (99,"esc") status
    else                                                ! [101] else convert ^x 
        do
            i += 1
            c = s[i;1]
            trace.print (99,"esc") i,c
            if asc(c) = 0 then
                exit
            elseif c # "^" then
                .fn += c
            else
                i += 1
                c = s[i;1]
                bval = asc(c)
                c = chr(bval - asc("A") + 1)
                .fn += c
            endif
        loop
    endif
    
EndFunction
