668 lines
14 KiB
NASM
668 lines
14 KiB
NASM
page ,132
|
||
; SCCSID = @(#)tenv.asm 4.2 85/08/16
|
||
; SCCSID = @(#)tenv.asm 4.2 85/08/16
|
||
TITLE Part6 COMMAND Transient routines.
|
||
;/*
|
||
; * Microsoft Confidential
|
||
; * Copyright (C) Microsoft Corporation 1991
|
||
; * All Rights Reserved.
|
||
; */
|
||
|
||
; Environment utilities and misc. routines
|
||
;
|
||
; Revision History
|
||
; ================
|
||
;
|
||
; M024 SR 9/5/90 Zero out comspec_flag to fix bug
|
||
; #710 about comspec getting trashed.
|
||
;
|
||
|
||
|
||
|
||
INCLUDE comsw.asm
|
||
|
||
.xlist
|
||
.xcref
|
||
include dossym.inc
|
||
include syscall.inc
|
||
include arena.inc
|
||
include comseg.asm
|
||
include comequ.asm
|
||
include doscntry.inc ;an000;
|
||
include resmsg.equ
|
||
.list
|
||
.cref
|
||
|
||
|
||
DATARES SEGMENT PUBLIC BYTE ;AC000;
|
||
EXTRN comdrv:byte
|
||
EXTRN comspec_end:word
|
||
EXTRN dbcs_vector_addr:dword ;AN000;
|
||
EXTRN ENVIRSEG:WORD
|
||
EXTRN fucase_addr:word ;AC000;
|
||
EXTRN PutBackDrv:byte
|
||
EXTRN PutBackComSpec:byte
|
||
EXTRN RESTDIR:BYTE
|
||
DATARES ENDS
|
||
|
||
TRANDATA SEGMENT PUBLIC BYTE ;AC000;
|
||
EXTRN arg_buf_ptr:word
|
||
EXTRN comspec:byte
|
||
EXTRN comspec_flag:byte
|
||
EXTRN comspecstr:byte
|
||
EXTRN ENVERR_PTR:WORD
|
||
EXTRN PATH_TEXT:byte
|
||
EXTRN PROMPT_TEXT:byte
|
||
EXTRN SYNTMES_PTR:WORD
|
||
TRANDATA ENDS
|
||
|
||
TRANSPACE SEGMENT PUBLIC BYTE ;AC000;
|
||
EXTRN Arg_Buf:BYTE
|
||
EXTRN RESSEG:WORD
|
||
EXTRN USERDIR1:BYTE
|
||
TRANSPACE ENDS
|
||
|
||
TRANCODE SEGMENT PUBLIC byte
|
||
|
||
ASSUME CS:TRANGROUP,DS:NOTHING,ES:NOTHING,SS:NOTHING
|
||
|
||
EXTRN cerror:near
|
||
|
||
PUBLIC add_name_to_environment
|
||
PUBLIC add_prompt
|
||
PUBLIC delete_path
|
||
PUBLIC find_name_in_environment
|
||
PUBLIC find_path
|
||
PUBLIC find_prompt
|
||
PUBLIC move_name
|
||
PUBLIC restudir
|
||
PUBLIC restudir1
|
||
PUBLIC scan_double_null
|
||
PUBLIC scasb2
|
||
PUBLIC store_char
|
||
PUBLIC Testkanj ;AN000; 3/3/KK
|
||
PUBLIC upconv
|
||
PUBLIC GETENVSIZ
|
||
|
||
BREAK <Environment utilities>
|
||
ASSUME DS:TRANGROUP
|
||
|
||
break Prompt command
|
||
assume ds:trangroup,es:trangroup
|
||
|
||
ADD_PROMPT:
|
||
CALL DELETE_PROMPT ; DELETE ANY EXISTING PROMPT
|
||
CALL SCAN_DOUBLE_NULL
|
||
|
||
ADD_PROMPT2:
|
||
PUSH SI
|
||
CALL GETARG
|
||
POP SI
|
||
retz ; PRE SCAN FOR ARGUMENTS
|
||
CALL MOVE_NAME ; MOVE IN NAME
|
||
CALL GETARG
|
||
PUSH SI
|
||
JMP SHORT ADD_NAME
|
||
|
||
|
||
break The SET command
|
||
assume ds:trangroup,es:trangroup
|
||
|
||
;
|
||
; Input: DS:SI points to a CR terminated string
|
||
; Output: carry flag is set if no room
|
||
; otherwise name is added to environment
|
||
;
|
||
|
||
DISP_ENVj:
|
||
jmp DISP_ENV
|
||
|
||
ADD_NAME_TO_ENVIRONMENT:
|
||
CALL GETARG
|
||
JZ DISP_ENVj
|
||
;
|
||
; check if line contains exactly one equals sign
|
||
;
|
||
XOR BX,BX ;= COUNT IS 0
|
||
PUSH SI ;SAVE POINTER TO BEGINNING OF LINE
|
||
|
||
EQLP:
|
||
LODSB ;GET A CHAR
|
||
CMP AL,13 ;IF CR WE'RE ALL DONE
|
||
JZ QUEQ
|
||
CMP AL,'=' ;LOOK FOR = SIGN
|
||
JNZ EQLP ;NOT THERE, GET NEXT CHAR
|
||
INC BL ;OTHERWISE INCREMENT EQ COUNT
|
||
CMP BYTE PTR [SI],13 ;LOOK FOR CR FOLLOWING = SIGN
|
||
JNZ EQLP
|
||
INC BH ;SET BH=1 MEANS NO PARAMETERS
|
||
JMP EQLP ;AND LOOK FOR MORE
|
||
|
||
QUEQ:
|
||
POP SI ;RESTORE BEGINNING OF LINE
|
||
DEC BL ;ZERO FLAG MEANS ONLY ONE EQ
|
||
JZ ONEQ ;GOOD LINE
|
||
MOV DX,OFFSET TRANGROUP:SYNTMES_ptr
|
||
JMP CERROR
|
||
|
||
ONEQ:
|
||
PUSH BX
|
||
CALL DELETE_NAME_IN_ENVIRONMENT
|
||
POP BX
|
||
DEC BH
|
||
retz
|
||
|
||
CALL SCAN_DOUBLE_NULL
|
||
mov bx,di ; Save ptr to beginning of env var name
|
||
CALL MOVE_NAME
|
||
push si
|
||
xchg bx,di ; Switch ptrs to beginning and end of
|
||
; env var name
|
||
;
|
||
; We want to special-case COMSPEC. This is to reduce the amount of code
|
||
; necessary in the resident for re-reading the transient. Let's look for
|
||
; COMSPEC=
|
||
;
|
||
mov comspec_flag,0 ; clear flag ; M024
|
||
mov si,offset trangroup:comspecstr ; Load ptr to string "COMSPEC"
|
||
mov cx,4 ; If the new env var is comspec, set
|
||
repz cmpsw ; the comspec_flag
|
||
;
|
||
; Zero set => exact match
|
||
;
|
||
jnz not_comspec
|
||
inc comspec_flag ; comspec is changing ; M024
|
||
|
||
not_comspec:
|
||
mov di,bx ; Load ptr to end of env var name
|
||
|
||
ADD_NAME: ; Add the value of the new env var
|
||
pop si ; to the environment.
|
||
push si
|
||
|
||
add_name1:
|
||
LODSB
|
||
CMP AL,13
|
||
jz add_name_ret
|
||
CALL STORE_CHAR
|
||
JMP ADD_NAME1
|
||
|
||
add_name_ret:
|
||
pop si
|
||
cmp comspec_flag,0 ; If the new env var is comspec,
|
||
retz ; copy the value into the
|
||
;
|
||
; We have changed the COMSPEC variable. We need to update the resident
|
||
; pieces necessary to reread in the info. First, skip all delimiters
|
||
;
|
||
invoke ScanOff
|
||
mov es,[resseg] ; comspec var in the resident
|
||
assume es:resgroup
|
||
;
|
||
; Make sure that the printer knows where the beginning of the string is
|
||
;
|
||
mov di,offset resgroup:comspec
|
||
mov bx,di
|
||
;
|
||
; Generate drive letter for display
|
||
;
|
||
xor ax,ax ;g assume no drive first
|
||
mov comdrv,al ;g
|
||
push ax ;AN000; 3/3/KK
|
||
mov al,[si] ;AN000; 3/3/KK
|
||
call testkanj ;AN000; 3/3/KK
|
||
pop ax ;AN000; 3/3/KK
|
||
jnz GotDrive
|
||
cmp byte ptr [si+1],':' ; drive specified?
|
||
jnz GotDrive
|
||
mov al,[si] ; get his specified drive
|
||
call UpConv ; convert to uppercase
|
||
sub al,'A' ; convert to 0-based
|
||
add di,2
|
||
inc al ; convert to 1-based number
|
||
mov comdrv,al
|
||
;
|
||
; Stick the drive letter in the prompt message. Nothing special needs to be
|
||
; done here..
|
||
;
|
||
|
||
add al,'A'-1
|
||
|
||
GotDrive: ;g
|
||
mov PutBackComSpec.SubstPtr,di ;g point to beginning of name after drive
|
||
mov es:PutBackDrv,al
|
||
;
|
||
; Copy chars until delim
|
||
;
|
||
|
||
mov di,bx
|
||
|
||
copy_comspec:
|
||
lodsb
|
||
invoke Delim
|
||
jz CopyDone
|
||
cmp al,13
|
||
jz CopyDone
|
||
stosb
|
||
jmp short copy_comspec
|
||
|
||
CopyDone:
|
||
xor al,al ; Null terminate the string and quit
|
||
stosb
|
||
mov comspec_flag,0
|
||
dec di
|
||
mov comspec_end,di
|
||
|
||
ret
|
||
|
||
DISP_ENV:
|
||
MOV DS,[RESSEG]
|
||
ASSUME DS:RESGROUP
|
||
MOV DS,[ENVIRSEG]
|
||
ASSUME DS:NOTHING
|
||
XOR SI,SI
|
||
|
||
PENVLP:
|
||
CMP BYTE PTR [SI],0
|
||
retz
|
||
mov di,offset trangroup:arg_buf
|
||
|
||
PENVLP2:
|
||
LODSB
|
||
stosb
|
||
OR AL,AL
|
||
JNZ PENVLP2
|
||
mov dx,offset trangroup:arg_buf_ptr
|
||
push ds
|
||
push es
|
||
pop ds
|
||
invoke printf_crlf
|
||
pop ds
|
||
JMP PENVLP
|
||
|
||
ASSUME DS:TRANGROUP
|
||
|
||
DELETE_PATH:
|
||
MOV SI,OFFSET TRANGROUP:PATH_TEXT
|
||
JMP SHORT DELETE_NAME_IN_environment
|
||
|
||
DELETE_PROMPT:
|
||
MOV SI,OFFSET TRANGROUP:PROMPT_TEXT
|
||
|
||
DELETE_NAME_IN_environment:
|
||
;
|
||
; Input: DS:SI points to a "=" terminated string
|
||
; Output: carry flag is set if name not found
|
||
; otherwise name is deleted
|
||
;
|
||
PUSH SI
|
||
PUSH DS
|
||
CALL FIND ; ES:DI POINTS TO NAME
|
||
JC DEL1
|
||
MOV SI,DI ; SAVE IT
|
||
CALL SCASB2 ; SCAN FOR THE NUL
|
||
XCHG SI,DI
|
||
;SR;
|
||
; If we have only one env string, then the double null is lost when the last
|
||
;string is deleted and we have an invalid empty environment with only a
|
||
;single null. To avoid this, we will look for the double null case and then
|
||
;move an extra null char.
|
||
; Bugbug: The only possible problem is that the last pathstring
|
||
;will be followed by a triple null. Is this really a problem?
|
||
;
|
||
cmp byte ptr es:[si],0 ;null char?
|
||
jnz not_dnull ;no, we are at a double null
|
||
dec si ;point at the double null
|
||
not_dnull:
|
||
|
||
CALL GETENVSIZ
|
||
SUB CX,SI
|
||
PUSH ES
|
||
POP DS ; ES:DI POINTS TO NAME, DS:SI POINTS TO NEXT NAME
|
||
REP MOVSB ; DELETE THE NAME
|
||
|
||
DEL1:
|
||
POP DS
|
||
POP SI
|
||
return
|
||
|
||
FIND_PATH:
|
||
MOV SI,OFFSET TRANGROUP:PATH_TEXT
|
||
JMP SHORT FIND_NAME_IN_environment
|
||
|
||
FIND_PROMPT:
|
||
MOV SI,OFFSET TRANGROUP:PROMPT_TEXT
|
||
|
||
FIND_NAME_IN_environment:
|
||
;
|
||
; Input: DS:SI points to a "=" terminated string
|
||
; Output: ES:DI points to the arguments in the environment
|
||
; zero is set if name not found
|
||
; carry flag is set if name not valid format
|
||
;
|
||
CALL FIND ; FIND THE NAME
|
||
retc ; CARRY MEANS NOT FOUND
|
||
JMP SCASB1 ; SCAN FOR = SIGN
|
||
;
|
||
; On return of FIND1, ES:DI points to beginning of name
|
||
;
|
||
FIND:
|
||
CLD
|
||
CALL COUNT0 ; CX = LENGTH OF NAME
|
||
MOV ES,[RESSEG]
|
||
ASSUME ES:RESGROUP
|
||
MOV ES,[ENVIRSEG]
|
||
ASSUME ES:NOTHING
|
||
XOR DI,DI
|
||
|
||
FIND1:
|
||
PUSH CX
|
||
PUSH SI
|
||
PUSH DI
|
||
|
||
FIND11:
|
||
LODSB
|
||
CALL TESTKANJ
|
||
JZ NOTKANJ3
|
||
DEC SI
|
||
LODSW
|
||
INC DI
|
||
INC DI
|
||
CMP AX,ES:[DI-2]
|
||
JNZ FIND12
|
||
DEC CX
|
||
LOOP FIND11
|
||
JMP SHORT FIND12
|
||
|
||
NOTKANJ3:
|
||
CALL UPCONV
|
||
INC DI
|
||
CMP AL,ES:[DI-1]
|
||
JNZ FIND12
|
||
LOOP FIND11
|
||
|
||
FIND12:
|
||
POP DI
|
||
POP SI
|
||
POP CX
|
||
retz
|
||
PUSH CX
|
||
CALL SCASB2 ; SCAN FOR A NUL
|
||
POP CX
|
||
CMP BYTE PTR ES:[DI],0
|
||
JNZ FIND1
|
||
STC ; INDICATE NOT FOUND
|
||
return
|
||
|
||
COUNT0:
|
||
PUSH DS
|
||
POP ES
|
||
MOV DI,SI
|
||
|
||
COUNT1:
|
||
PUSH DI ; COUNT NUMBER OF CHARS UNTIL "="
|
||
CALL SCASB1
|
||
JMP SHORT COUNTX
|
||
|
||
COUNT2:
|
||
PUSH DI ; COUNT NUMBER OF CHARS UNTIL NUL
|
||
CALL SCASB2
|
||
|
||
COUNTX:
|
||
POP CX
|
||
SUB DI,CX
|
||
XCHG DI,CX
|
||
return
|
||
|
||
MOVE_NAME:
|
||
CMP BYTE PTR DS:[SI],13
|
||
retz
|
||
LODSB
|
||
|
||
;;;; IFDEF DBCS 3/3/KK
|
||
CALL TESTKANJ
|
||
JZ NOTKANJ1
|
||
CALL STORE_CHAR
|
||
LODSB
|
||
CALL STORE_CHAR
|
||
JMP SHORT MOVE_NAME
|
||
|
||
NOTKANJ1:
|
||
;;;; ENDIF 3/3/KK
|
||
|
||
CALL UPCONV
|
||
CALL STORE_CHAR
|
||
CMP AL,'='
|
||
JNZ MOVE_NAME
|
||
return
|
||
|
||
GETARG:
|
||
MOV SI,80H
|
||
LODSB
|
||
OR AL,AL
|
||
retz
|
||
invoke SCANOFF
|
||
CMP AL,13
|
||
return
|
||
|
||
;
|
||
; Point ES:DI to the final NULL string. Note that in an empty environment,
|
||
; there is NO double NULL, merely a string that is empty.
|
||
;
|
||
SCAN_DOUBLE_NULL:
|
||
MOV ES,[RESSEG]
|
||
ASSUME ES:RESGROUP
|
||
MOV ES,[ENVIRSEG]
|
||
ASSUME ES:NOTHING
|
||
XOR DI,DI
|
||
;
|
||
; Top cycle-point. If the string here is empty, then we are done
|
||
;
|
||
SDN1:
|
||
cmp byte ptr es:[di],0 ; nul string?
|
||
retz ; yep, all done
|
||
CALL SCASB2
|
||
JMP SDN1
|
||
|
||
SCASB1:
|
||
MOV AL,'=' ; SCAN FOR AN =
|
||
JMP SHORT SCASBX
|
||
SCASB2:
|
||
XOR AL,AL ; SCAN FOR A NUL
|
||
SCASBX:
|
||
MOV CX,1000H
|
||
REPNZ SCASB
|
||
return
|
||
;Bugbug: This is Kanji stuff - put it in conditionals
|
||
|
||
TESTKANJ:
|
||
push ds ;AN000; 3/3/KK
|
||
push si ;AN000; 3/3/KK
|
||
push ax ;AN000; 3/3/KK
|
||
mov ds,cs:[resseg] ;AN000; Get resident segment
|
||
assume ds:resgroup ;AN000;
|
||
lds si,dbcs_vector_addr ;AN000; get DBCS vector
|
||
ktlop: ;AN000; 3/3/KK
|
||
cmp word ptr ds:[si],0 ;AN000; end of Table 3/3/KK
|
||
je notlead ;AN000; 3/3/KK
|
||
pop ax ;AN000; 3/3/KK
|
||
push ax ;AN000; 3/3/KK
|
||
cmp al, byte ptr ds:[si] ;AN000; 3/3/KK
|
||
jb notlead ;AN000; 3/3/KK
|
||
inc si ;AN000; 3/3/KK
|
||
cmp al, byte ptr ds:[si] ;AN000; 3/3/KK
|
||
jbe islead ;AN000; 3/3/KK
|
||
inc si ;AN000; 3/3/KK
|
||
jmp short ktlop ;AN000; try another range ; 3/3/KK
|
||
Notlead: ;AN000; 3/3/KK
|
||
xor ax,ax ;AN000; set zero 3/3/KK
|
||
jmp short ktret ;AN000; 3/3/KK
|
||
Islead: ;AN000; 3/3/KK
|
||
xor ax,ax ;AN000; reset zero 3/3/KK
|
||
inc ax ;AN000; 3/3/KK
|
||
ktret: ;AN000; 3/3/KK
|
||
pop ax ;AN000; 3/3/KK
|
||
pop si ;AN000; 3/3/KK
|
||
pop ds ;AN000; 3/3/KK
|
||
return ;AN000; 3/3/KK
|
||
;------------------------------------- ;3/3/KK
|
||
|
||
|
||
; ****************************************************************
|
||
; *
|
||
; * ROUTINE: UPCONV (ADDED BY EMG 4.00)
|
||
; *
|
||
; * FUNCTION: This routine returns the upper case equivalent of
|
||
; * the character in AL from the file upper case table
|
||
; * in DOS if character if above ascii 128, else
|
||
; * subtracts 20H if between "a" and "z".
|
||
; *
|
||
; * INPUT: AL char to be upper cased
|
||
; * FUCASE_ADDR set to the file upper case table
|
||
; *
|
||
; * OUTPUT: AL upper cased character
|
||
; *
|
||
; ****************************************************************
|
||
|
||
assume ds:trangroup ;AN000;
|
||
|
||
upconv proc near ;AN000;
|
||
|
||
cmp al,80h ;AN000; see if char is > ascii 128
|
||
jb oth_fucase ;AN000; no - upper case math
|
||
sub al,80h ;AN000; only upper 128 chars in table
|
||
push ds ;AN000;
|
||
push bx ;AN000;
|
||
mov ds,[resseg] ;AN000; get resident data segment
|
||
assume ds:resgroup ;AN000;
|
||
lds bx,dword ptr fucase_addr+1 ;AN000; get table address
|
||
add bx,2 ;AN000; skip over first word
|
||
xlat ds:byte ptr [bx] ;AN000; convert to upper case
|
||
pop bx ;AN000;
|
||
pop ds ;AN000;
|
||
assume ds:trangroup ;AN000;
|
||
jmp short upconv_end ;AN000; we finished - exit
|
||
|
||
oth_fucase: ;AN000;
|
||
cmp al,small_a ;AC000; if between "a" and "z",
|
||
jb upconv_end ;AC000; subtract 20h to get
|
||
cmp al,small_z ;AC000; upper case equivalent.
|
||
ja upconv_end ;AC000;
|
||
sub al,20h ;AC000; Change lower-case to upper
|
||
|
||
upconv_end: ;AN000;
|
||
ret
|
||
|
||
upconv endp ;AN000;
|
||
|
||
|
||
;
|
||
; STORE A CHAR IN environment, GROWING IT IF NECESSARY
|
||
;
|
||
STORE_CHAR:
|
||
PUSH CX
|
||
PUSH BX
|
||
PUSH ES ;AN056;
|
||
PUSH DS ;AN056; Save local DS
|
||
MOV DS,[RESSEG] ;AN056; Get resident segment
|
||
ASSUME DS:RESGROUP ;AN056;
|
||
MOV ES,[ENVIRSEG] ;AN056; Get environment segment
|
||
ASSUME ES:NOTHING ;AN056;
|
||
POP DS ;AN056; Get local segment back
|
||
ASSUME DS:TRANGROUP ;AN056;
|
||
CALL GETENVSIZ
|
||
MOV BX,CX
|
||
SUB BX,2 ; SAVE ROOM FOR DOUBLE NULL
|
||
CMP DI,BX
|
||
JB STORE1
|
||
|
||
PUSH AX
|
||
PUSH CX
|
||
PUSH BX ; Save Size of environment
|
||
invoke FREE_TPA
|
||
POP BX
|
||
ADD BX,2 ; Recover true environment size
|
||
|
||
CMP BX, 8000H ; Don't let environment grow > 32K
|
||
JB ENVSIZ_OK
|
||
BAD_ENV_SIZE: ;AN056;
|
||
STC
|
||
JMP SHORT ENVNOSET
|
||
ENVSIZ_OK:
|
||
|
||
MOV CL,4
|
||
SHR BX,CL ; Convert back to paragraphs
|
||
INC BX ; Try to grow environment by one para
|
||
MOV CX,ES ;AN056; Get environment segment
|
||
ADD CX,BX ;AN056; Add in size of environment
|
||
ADD CX,020H ;AN056; Add in some TPA
|
||
MOV AX,CS ;AN056; Get the transient segment
|
||
CMP CX,AX ;AN056; Are we hitting the transient?
|
||
JNB BAD_ENV_SIZE ;AN056; Yes - don't do it!!!
|
||
MOV AH,SETBLOCK
|
||
INT 21h
|
||
ENVNOSET:
|
||
PUSHF
|
||
PUSH ES
|
||
MOV ES,[RESSEG]
|
||
invoke ALLOC_TPA
|
||
POP ES
|
||
POPF
|
||
POP CX
|
||
POP AX
|
||
JNC STORE1
|
||
POP ES ;AN056;
|
||
MOV DX,OFFSET TRANGROUP:ENVERR_ptr
|
||
JMP CERROR
|
||
STORE1:
|
||
STOSB
|
||
MOV WORD PTR ES:[DI],0 ; NULL IS AT END
|
||
POP ES ;AN056;
|
||
POP BX
|
||
POP CX
|
||
return
|
||
|
||
GETENVSIZ:
|
||
;Get size of environment in bytes, rounded up to paragraph boundry
|
||
;ES has environment segment
|
||
;Size returned in CX, all other registers preserved
|
||
|
||
PUSH ES
|
||
PUSH AX
|
||
MOV AX,ES
|
||
DEC AX ;Point at arena
|
||
MOV ES,AX
|
||
MOV AX,ES:[arena_size]
|
||
MOV CL,4
|
||
SHL AX,CL ;Convert to bytes
|
||
MOV CX,AX
|
||
POP AX
|
||
POP ES
|
||
return
|
||
|
||
|
||
ASSUME DS:TRANGROUP
|
||
|
||
|
||
RESTUDIR1:
|
||
PUSH DS
|
||
MOV DS,[RESSEG]
|
||
ASSUME DS:RESGROUP
|
||
CMP [RESTDIR],0
|
||
POP DS
|
||
ASSUME DS:TRANGROUP
|
||
retz
|
||
|
||
RESTUDIR:
|
||
MOV DX,OFFSET TRANGROUP:USERDIR1
|
||
MOV AH,CHDIR
|
||
INT 21h ; Restore users DIR
|
||
XOR AL,AL
|
||
invoke SETREST
|
||
RET56:
|
||
return
|
||
|
||
trancode ends
|
||
end
|
||
|