Add an implementation of the POSIX wordexp() and wordfree() functions,
which perform shell-style word expansion on strings. This is still a little rough around the edges. PR: 13420
This commit is contained in:
parent
56f562526c
commit
faea1495bf
Notes:
svn2git
2020-12-20 02:59:44 +00:00
svn path=/head/; revision=108288
@ -29,7 +29,7 @@ SRCS+= __xuname.c _pthread_stubs.c _rand48.c _spinlock_stub.c _thread_init.c \
|
||||
sysconf.c sysctl.c sysctlbyname.c sysctlnametomib.c \
|
||||
syslog.c telldir.c termios.c time.c times.c timezone.c ttyname.c \
|
||||
ttyslot.c ualarm.c ulimit.c uname.c unvis.c usleep.c utime.c \
|
||||
valloc.c vis.c wait.c wait3.c waitpid.c
|
||||
valloc.c vis.c wait.c wait3.c waitpid.c wordexp.c
|
||||
|
||||
# machine-dependent gen sources
|
||||
.if exists(${.CURDIR}/${MACHINE_ARCH}/gen/Makefile.inc)
|
||||
@ -56,7 +56,7 @@ MAN+= alarm.3 arc4random.3 \
|
||||
strtofflags.3 sysconf.3 sysctl.3 syslog.3 tcgetpgrp.3 \
|
||||
tcsendbreak.3 tcsetattr.3 tcsetpgrp.3 time.3 times.3 timezone.3 \
|
||||
ttyname.3 tzset.3 ualarm.3 ucontext.3 ulimit.3 uname.3 \
|
||||
unvis.3 usleep.3 utime.3 valloc.3 vis.3
|
||||
unvis.3 usleep.3 utime.3 valloc.3 vis.3 wordexp.3
|
||||
|
||||
MLINKS+=arc4random.3 arc4random_addrandom.3 arc4random.3 arc4random_stir.3
|
||||
MLINKS+=ctermid.3 ctermid_r.3
|
||||
@ -131,3 +131,4 @@ MLINKS+=ttyname.3 isatty.3 ttyname.3 ttyslot.3
|
||||
MLINKS+=tzset.3 tzsetwall.3
|
||||
MLINKS+=unvis.3 strunvis.3 unvis.3 strunvisx.3
|
||||
MLINKS+=vis.3 strvis.3 vis.3 strvisx.3
|
||||
MLINKS+=wordexp.3 wordfree.3
|
||||
|
207
lib/libc/gen/wordexp.3
Normal file
207
lib/libc/gen/wordexp.3
Normal file
@ -0,0 +1,207 @@
|
||||
.\"
|
||||
.\" Copyright (c) 2002 Tim J. Robbins
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
.\" Redistribution and use in source and binary forms, with or without
|
||||
.\" modification, are permitted provided that the following conditions
|
||||
.\" are met:
|
||||
.\" 1. Redistributions of source code must retain the above copyright
|
||||
.\" notice, this list of conditions and the following disclaimer.
|
||||
.\" 2. Redistributions in binary form must reproduce the above copyright
|
||||
.\" notice, this list of conditions and the following disclaimer in the
|
||||
.\" documentation and/or other materials provided with the distribution.
|
||||
.\"
|
||||
.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
.\" SUCH DAMAGE.
|
||||
.\"
|
||||
.\" $FreeBSD$
|
||||
.\"
|
||||
.Dd December 27, 2002
|
||||
.Dt WORDEXP 3
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm wordexp
|
||||
.Nd "perform shell-style word expansions"
|
||||
.Sh LIBRARY
|
||||
.Lb libc
|
||||
.Sh SYNOPSIS
|
||||
.In sys/types.h
|
||||
.In wordexp.h
|
||||
.Ft int
|
||||
.Fo wordexp
|
||||
.Fa "const char * restrict words"
|
||||
.Fa "wordexp_t * restrict we"
|
||||
.Fa "int flags"
|
||||
.Fc
|
||||
.Fo wordfree
|
||||
.Fa "wordexp_t *we"
|
||||
.Fc
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Fn wordexp
|
||||
function performs shell-style word expansion on
|
||||
.Fa words
|
||||
and places the list of words into the
|
||||
.Va we_wordv
|
||||
member of
|
||||
.Va we ,
|
||||
and the number of words into
|
||||
.Va we_wordc .
|
||||
.Pp
|
||||
The
|
||||
.Va flags
|
||||
argument is the bitwise inclusive OR of any of the following constants:
|
||||
.Bl -tag -width ".Dv WRDE_SHOWERR"
|
||||
.It Dv WRDE_APPEND
|
||||
Append the words to those generate by a previous call to
|
||||
.Fn wordexp .
|
||||
.It Dv WRDE_DOOFS
|
||||
As many
|
||||
.Dv NULL
|
||||
pointers as are specified by the
|
||||
.Va we_offs
|
||||
member of
|
||||
.Va we
|
||||
are added to the front of
|
||||
.Va we_wordv .
|
||||
.It Dv WRDE_NOCMD
|
||||
Disallow command substitution in
|
||||
.Fa words .
|
||||
See the note in
|
||||
.Sx BUGS
|
||||
before using this.
|
||||
.It Dv WRDE_REUSE
|
||||
The
|
||||
.Va we
|
||||
argument was passed to a previous successful call to
|
||||
.Fn wordexp
|
||||
but has not been passed to
|
||||
.Fn wordfree .
|
||||
The implementation may reuse the space allocated to it.
|
||||
.It Dv WRDE_SHOWERR
|
||||
Do not redirect shell error messages to
|
||||
.Pa /dev/null .
|
||||
.It Dv WRDE_UNDEF
|
||||
Report error on an attempt to expand an undefined shell variable.
|
||||
.El
|
||||
.Pp
|
||||
The
|
||||
.Vt wordexp_t
|
||||
structure is defined in
|
||||
.Pa wordexp.h
|
||||
as:
|
||||
.Bd -literal -offset indent
|
||||
typedef struct {
|
||||
size_t we_wordc; /* count of words matched */
|
||||
char **we_wordv; /* pointer to list of words */
|
||||
size_t we_offs; /* slots to reserve in we_wordv */
|
||||
} wordexp_t;
|
||||
.Ed
|
||||
.Pp
|
||||
The
|
||||
.Fn wordfree
|
||||
function frees the memory allocated by
|
||||
.Fn wordexp .
|
||||
.Sh IMPLEMENTATION NOTES
|
||||
The
|
||||
.Fn wordexp
|
||||
function is implemented as a wrapper around the undocumented
|
||||
.Ic wordexp
|
||||
shell built-in command.
|
||||
.Sh RETURN VALUES
|
||||
The
|
||||
.Fn wordexp
|
||||
function returns zero if successful, otherwise it returns one of the following
|
||||
error codes:
|
||||
.Bl -tag -width ".Dv WRDE_NOSPACE"
|
||||
.It Dv WRDE_BADCHAR
|
||||
The
|
||||
.Fa words
|
||||
argument contains one of the following unquoted characters:
|
||||
<newline>,
|
||||
.Ql | ,
|
||||
.Ql & ,
|
||||
.Ql \&; ,
|
||||
.Ql < ,
|
||||
.Ql > ,
|
||||
.Ql \&( ,
|
||||
.Ql \&) ,
|
||||
.Ql { ,
|
||||
.Ql } .
|
||||
.It Dv WRDE_BADVAL
|
||||
An attempt was made to expand an undefined shell variable and
|
||||
.Dv WRDE_UNDEF
|
||||
is set in
|
||||
.Fa flags .
|
||||
.It Dv WRDE_CMDSUB
|
||||
An attempt was made to use command substitution and
|
||||
.Dv WRDE_NOCMD
|
||||
is set in
|
||||
.Fa flags .
|
||||
.It Dv WRDE_NOSPACE
|
||||
Not enough memory to store the result.
|
||||
.It Dv WRDE_SYNTAX
|
||||
Shell syntax error in
|
||||
.Fa words .
|
||||
.El
|
||||
.Pp
|
||||
The
|
||||
.Fn wordfree
|
||||
function returns no value.
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width ".Ev IFS"
|
||||
.It Ev IFS
|
||||
Field separator.
|
||||
.El
|
||||
.Sh EXAMPLES
|
||||
Invoke the editor on all
|
||||
.Dq Li \&.c
|
||||
files in the current directory,
|
||||
and
|
||||
.Pa /etc/motd
|
||||
(error checking omitted):
|
||||
.Bd -literal -offset indent
|
||||
wordexp_t we;
|
||||
|
||||
wordexp("${EDITOR:-vi} *.c /etc/motd", &we, 0);
|
||||
execvp(we->we_wordv[0], we->we_wordv);
|
||||
.Ed
|
||||
.Sh DIAGNOSTICS
|
||||
Diagnostic messages from the shell are written to the standard error output
|
||||
if
|
||||
.Dv WRDE_SHOWERR
|
||||
is set in
|
||||
.Fa flags .
|
||||
.Sh SEE ALSO
|
||||
.Xr sh 1 ,
|
||||
.Xr fnmatch 3 ,
|
||||
.Xr glob 3 ,
|
||||
.Xr popen 3 ,
|
||||
.Xr system 3
|
||||
.Sh STANDARDS
|
||||
The
|
||||
.Fn wordexp
|
||||
and
|
||||
.Fn wordfree
|
||||
functions conforms to
|
||||
.St -p1003.1-2001 .
|
||||
.Sh BUGS
|
||||
Do not pass untrusted user data to
|
||||
.Fn wordexp ,
|
||||
regardless of whether the
|
||||
.Dv WRDE_NOCMD
|
||||
flag is set.
|
||||
The
|
||||
.Fn wordexp
|
||||
function attempts to detect input that would cause commands to be
|
||||
executed before passing it to the shell
|
||||
but it does not use the same parser so it may be fooled.
|
308
lib/libc/gen/wordexp.c
Normal file
308
lib/libc/gen/wordexp.c
Normal file
@ -0,0 +1,308 @@
|
||||
/*-
|
||||
* Copyright (c) 2002 Tim J. Robbins.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "namespace.h"
|
||||
#include <sys/cdefs.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <paths.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <wordexp.h>
|
||||
#include "un-namespace.h"
|
||||
|
||||
__FBSDID("$FreeBSD$");
|
||||
|
||||
static int we_askshell(const char *, wordexp_t *, int);
|
||||
static int we_check(const char *, int);
|
||||
|
||||
/*
|
||||
* wordexp --
|
||||
* Perform shell word expansion on `words' and place the resulting list
|
||||
* of words in `we'. See wordexp(3).
|
||||
*
|
||||
* Specified by IEEE Std. 1003.1-2001.
|
||||
*/
|
||||
int
|
||||
wordexp(const char * __restrict words, wordexp_t * __restrict we, int flags)
|
||||
{
|
||||
int error;
|
||||
|
||||
if (flags & WRDE_REUSE)
|
||||
wordfree(we);
|
||||
if ((flags & WRDE_APPEND) == 0) {
|
||||
we->we_wordc = 0;
|
||||
we->we_wordv = NULL;
|
||||
we->we_strings = NULL;
|
||||
we->we_nbytes = 0;
|
||||
}
|
||||
if ((error = we_check(words, flags)) != 0) {
|
||||
wordfree(we);
|
||||
return (error);
|
||||
}
|
||||
if ((error = we_askshell(words, we, flags)) != 0) {
|
||||
wordfree(we);
|
||||
return (error);
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
|
||||
/*
|
||||
* we_askshell --
|
||||
* Use the `wordexp' /bin/sh builtin function to do most of the work
|
||||
* in expanding the word string. This function is complicated by
|
||||
* memory management.
|
||||
*/
|
||||
static int
|
||||
we_askshell(const char *words, wordexp_t *we, int flags)
|
||||
{
|
||||
int pdes[2]; /* Pipe to child */
|
||||
char bbuf[9]; /* Buffer for byte count */
|
||||
char wbuf[9]; /* Buffer for word count */
|
||||
long nwords, nbytes; /* Number of words, bytes from child */
|
||||
long i; /* Handy integer */
|
||||
size_t sofs; /* Offset into we->we_strings */
|
||||
size_t vofs; /* Offset into we->we_wordv */
|
||||
pid_t pid; /* Process ID of child */
|
||||
int status; /* Child exit status */
|
||||
char *ifs; /* IFS env. var. */
|
||||
char *np, *p; /* Handy pointers */
|
||||
char *nstrings; /* Temporary for realloc() */
|
||||
char **nwv; /* Temporary for realloc() */
|
||||
|
||||
if ((ifs = getenv("IFS")) == NULL)
|
||||
ifs = " \t\n";
|
||||
|
||||
if (pipe(pdes) < 0)
|
||||
return (WRDE_NOSPACE); /* XXX */
|
||||
if ((pid = fork()) < 0) {
|
||||
close(pdes[0]);
|
||||
close(pdes[1]);
|
||||
return (WRDE_NOSPACE); /* XXX */
|
||||
}
|
||||
else if (pid == 0) {
|
||||
/*
|
||||
* We are the child; just get /bin/sh to run the wordexp
|
||||
* builtin on `words'.
|
||||
*/
|
||||
int devnull;
|
||||
char *cmd;
|
||||
|
||||
close(pdes[0]);
|
||||
if (dup2(pdes[1], STDOUT_FILENO) < 0)
|
||||
_exit(1);
|
||||
close(pdes[1]);
|
||||
if (asprintf(&cmd, "wordexp%c%s\n", *ifs, words) < 0)
|
||||
_exit(1);
|
||||
if ((flags & WRDE_SHOWERR) == 0) {
|
||||
if ((devnull = open(_PATH_DEVNULL, O_RDWR, 0666)) < 0)
|
||||
_exit(1);
|
||||
if (dup2(devnull, STDERR_FILENO) < 0)
|
||||
_exit(1);
|
||||
close(devnull);
|
||||
}
|
||||
execl(_PATH_BSHELL, "sh", flags & WRDE_UNDEF ? "-u" : "+u",
|
||||
"-c", cmd, NULL);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* We are the parent; read the output of the shell wordexp function,
|
||||
* which is a 32-bit hexadecimal word count, a 32-bit hexadecimal
|
||||
* byte count (not including terminating null bytes), followed by
|
||||
* the expanded words separated by nulls.
|
||||
*/
|
||||
close(pdes[1]);
|
||||
if (read(pdes[0], wbuf, 8) != 8 || read(pdes[0], bbuf, 8) != 8) {
|
||||
close(pdes[0]);
|
||||
return (WRDE_NOSPACE); /* XXX */
|
||||
}
|
||||
wbuf[8] = bbuf[8] = '\0';
|
||||
nwords = strtol(wbuf, NULL, 16);
|
||||
nbytes = strtol(bbuf, NULL, 16) + nwords;
|
||||
|
||||
/*
|
||||
* Allocate or reallocate (when flags & WRDE_APPEND) the word vector
|
||||
* and string storage buffers for the expanded words we're about to
|
||||
* read from the child.
|
||||
*/
|
||||
sofs = we->we_nbytes;
|
||||
vofs = we->we_wordc;
|
||||
we->we_wordc += nwords;
|
||||
we->we_nbytes += nbytes;
|
||||
if ((nwv = realloc(we->we_wordv, (we->we_wordc + 1 +
|
||||
(flags & WRDE_DOOFS ? we->we_offs : 0)) *
|
||||
sizeof(char *))) == NULL) {
|
||||
close(pdes[0]);
|
||||
return (WRDE_NOSPACE);
|
||||
}
|
||||
we->we_wordv = nwv;
|
||||
if ((nstrings = realloc(we->we_strings, we->we_nbytes)) == NULL) {
|
||||
close(pdes[0]);
|
||||
return (WRDE_NOSPACE);
|
||||
}
|
||||
for (i = 0; i < vofs; i++)
|
||||
if (we->we_wordv[i] != NULL)
|
||||
we->we_wordv[i] += nstrings - we->we_strings;
|
||||
we->we_strings = nstrings;
|
||||
|
||||
if (read(pdes[0], we->we_strings + sofs, nbytes) != nbytes) {
|
||||
close(pdes[0]);
|
||||
return (WRDE_NOSPACE); /* XXX */
|
||||
}
|
||||
|
||||
if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) ||
|
||||
WEXITSTATUS(status) != 0) {
|
||||
close(pdes[0]);
|
||||
return (flags & WRDE_UNDEF ? WRDE_BADVAL : WRDE_SYNTAX);
|
||||
}
|
||||
close(pdes[0]);
|
||||
|
||||
/*
|
||||
* Break the null-terminated expanded word strings out into
|
||||
* the vector.
|
||||
*/
|
||||
if (vofs == 0 && flags & WRDE_DOOFS)
|
||||
while (vofs < we->we_offs)
|
||||
we->we_wordv[vofs++] = NULL;
|
||||
p = we->we_strings + sofs;
|
||||
while (nwords-- != 0) {
|
||||
we->we_wordv[vofs++] = p;
|
||||
if ((np = memchr(p, '\0', nbytes)) == NULL)
|
||||
return (WRDE_NOSPACE); /* XXX */
|
||||
nbytes -= np - p + 1;
|
||||
p = np + 1;
|
||||
}
|
||||
we->we_wordv[vofs] = NULL;
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
/*
|
||||
* we_check --
|
||||
* Check that the string contains none of the following unquoted
|
||||
* special characters: <newline> |&;<>(){}
|
||||
* or command substitutions when WRDE_NOCMD is set in flags.
|
||||
*/
|
||||
int
|
||||
we_check(const char *words, int flags)
|
||||
{
|
||||
char c;
|
||||
int dquote, level, quote, squote;
|
||||
|
||||
quote = squote = dquote = 0;
|
||||
while ((c = *words++) != '\0') {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
quote ^= 1;
|
||||
break;
|
||||
case '\'':
|
||||
if (quote + dquote == 0)
|
||||
squote ^= 1;
|
||||
break;
|
||||
case '"':
|
||||
if (quote + squote == 0)
|
||||
dquote ^= 1;
|
||||
break;
|
||||
case '`':
|
||||
if (quote + squote == 0 && flags & WRDE_NOCMD)
|
||||
return (WRDE_CMDSUB);
|
||||
while ((c = *words++) != '\0' && c != '`')
|
||||
if (c == '\\' && (c = *words++) == '\0')
|
||||
break;
|
||||
if (c == '\0')
|
||||
return (WRDE_SYNTAX);
|
||||
break;
|
||||
case '|': case '&': case ';': case '<': case '>':
|
||||
case '{': case '}': case '(': case ')': case '\n':
|
||||
if (quote + squote + dquote == 0)
|
||||
return (WRDE_BADCHAR);
|
||||
break;
|
||||
case '$':
|
||||
if ((c = *words++) == '\0')
|
||||
break;
|
||||
else if (c == '(') {
|
||||
if (flags & WRDE_NOCMD)
|
||||
return (WRDE_CMDSUB);
|
||||
level = 1;
|
||||
while ((c = *words++) != '\0') {
|
||||
if (c == '\\') {
|
||||
if ((c = *words++) == '\0')
|
||||
break;
|
||||
} else if (c == '(')
|
||||
level++;
|
||||
else if (c == ')' && --level == 0)
|
||||
break;
|
||||
}
|
||||
if (c == '\0' || level != 0)
|
||||
return (WRDE_SYNTAX);
|
||||
} else if (c == '{') {
|
||||
level = 1;
|
||||
while ((c = *words++) != '\0') {
|
||||
if (c == '\\') {
|
||||
if ((c = *words++) == '\0')
|
||||
break;
|
||||
} else if (c == '{')
|
||||
level++;
|
||||
else if (c == '}' && --level == 0)
|
||||
break;
|
||||
}
|
||||
if (c == '\0' || level != 0)
|
||||
return (WRDE_SYNTAX);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (quote + squote + dquote != 0)
|
||||
return (WRDE_SYNTAX);
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
/*
|
||||
* wordfree --
|
||||
* Free the result of wordexp(). See wordexp(3).
|
||||
*
|
||||
* Specified by IEEE Std. 1003.1-2001.
|
||||
*/
|
||||
void
|
||||
wordfree(wordexp_t *we)
|
||||
{
|
||||
|
||||
if (we == NULL)
|
||||
return;
|
||||
free(we->we_wordv);
|
||||
free(we->we_strings);
|
||||
we->we_wordv = NULL;
|
||||
we->we_strings = NULL;
|
||||
we->we_nbytes = 0;
|
||||
we->we_wordc = 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user