Relicense Sortix to the ISC license.
I hereby relicense all my work on Sortix under the ISC license as below.
All Sortix contributions by other people are already under this license,
are not substantial enough to be copyrightable, or have been removed.
All imported code from other projects is compatible with this license.
All GPL licensed code from other projects had previously been removed.
Copyright 2011-2016 Jonas 'Sortie' Termansen and contributors.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2016-03-02 17:38:16 -05:00
|
|
|
/*
|
2020-10-17 11:31:22 -04:00
|
|
|
* Copyright (c) 2014, 2020 Jonas 'Sortie' Termansen.
|
Relicense Sortix to the ISC license.
I hereby relicense all my work on Sortix under the ISC license as below.
All Sortix contributions by other people are already under this license,
are not substantial enough to be copyrightable, or have been removed.
All imported code from other projects is compatible with this license.
All GPL licensed code from other projects had previously been removed.
Copyright 2011-2016 Jonas 'Sortie' Termansen and contributors.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2016-03-02 17:38:16 -05:00
|
|
|
*
|
|
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
|
|
* copyright notice and this permission notice appear in all copies.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
*
|
|
|
|
* chmod.c
|
2020-10-17 11:31:22 -04:00
|
|
|
* Change file permissions.
|
Relicense Sortix to the ISC license.
I hereby relicense all my work on Sortix under the ISC license as below.
All Sortix contributions by other people are already under this license,
are not substantial enough to be copyrightable, or have been removed.
All imported code from other projects is compatible with this license.
All GPL licensed code from other projects had previously been removed.
Copyright 2011-2016 Jonas 'Sortie' Termansen and contributors.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2016-03-02 17:38:16 -05:00
|
|
|
*/
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
|
|
#include <assert.h>
|
|
|
|
#include <dirent.h>
|
2020-10-17 11:31:22 -04:00
|
|
|
#include <err.h>
|
2013-06-18 07:40:12 -04:00
|
|
|
#include <errno.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <inttypes.h>
|
2016-02-28 18:40:20 -05:00
|
|
|
#include <stdbool.h>
|
2013-06-18 07:40:12 -04:00
|
|
|
#include <stddef.h>
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
static const int FLAG_CHANGES = 1 << 0;
|
|
|
|
static const int FLAG_VERBOSE = 1 << 1;
|
|
|
|
static const int FLAG_RECURSIVE = 1 << 2;
|
2020-10-17 11:31:22 -04:00
|
|
|
static const int FLAG_NOFOLLOW = 1 << 3;
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
enum symderef
|
|
|
|
{
|
|
|
|
SYMDEREF_NONE,
|
|
|
|
SYMDEREF_ARGUMENTS,
|
|
|
|
SYMDEREF_ALWAYS,
|
|
|
|
SYMDEREF_DEFAULT,
|
|
|
|
};
|
|
|
|
|
|
|
|
static bool is_octal_string(const char* str)
|
|
|
|
{
|
|
|
|
if ( !str[0] )
|
|
|
|
return false;
|
|
|
|
for ( size_t i = 0; str[i]; i++ )
|
|
|
|
if ( !('0' <= str[i] && str[i] <= '7') )
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static mode_t execute_modespec(const char* str,
|
|
|
|
mode_t mode,
|
|
|
|
mode_t type,
|
|
|
|
mode_t umask)
|
|
|
|
{
|
|
|
|
if ( is_octal_string(str) )
|
|
|
|
{
|
|
|
|
errno = 0;
|
|
|
|
uintmax_t input = strtoumax((char*) str, NULL, 8);
|
|
|
|
if ( errno == ERANGE )
|
|
|
|
return (mode_t) -1;
|
|
|
|
if ( input & ~((uintmax_t) 07777) )
|
|
|
|
return (mode_t) -1;
|
|
|
|
return (mode_t) input;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t index = 0;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
mode_t who_mask = 01000;
|
|
|
|
while ( true )
|
|
|
|
{
|
|
|
|
if ( str[index] == 'u' && (index++, true) )
|
|
|
|
who_mask |= 04700;
|
|
|
|
else if ( str[index] == 'g' && (index++, true) )
|
|
|
|
who_mask |= 02070;
|
|
|
|
else if ( str[index] == 'o' && (index++, true) )
|
|
|
|
who_mask |= 00007;
|
|
|
|
else if ( str[index] == 'a' && (index++, true) )
|
|
|
|
who_mask |= 06777;
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if ( !(who_mask & 0777) )
|
|
|
|
who_mask |= 06777 & ~umask;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
char op;
|
|
|
|
switch ( (op = str[index++]) )
|
|
|
|
{
|
|
|
|
case '+': break;
|
|
|
|
case '-': break;
|
|
|
|
case '=': break;
|
|
|
|
default: return (mode_t) -1;
|
|
|
|
};
|
|
|
|
mode_t operand = 0;
|
|
|
|
if ( str[index] == 'u' || str[index] == 'g' || str[index] == 'o' )
|
|
|
|
{
|
|
|
|
char permcopy = str[index++];
|
|
|
|
switch ( permcopy )
|
|
|
|
{
|
|
|
|
case 'u': operand = mode >> 6 & 07; break;
|
|
|
|
case 'g': operand = mode >> 3 & 07; break;
|
|
|
|
case 'o': operand = mode >> 0 & 07; break;
|
|
|
|
default: __builtin_unreachable();
|
|
|
|
};
|
|
|
|
operand = operand << 0 | operand << 3 | operand << 6;
|
|
|
|
switch ( permcopy )
|
|
|
|
{
|
|
|
|
case 'u': if ( mode & 04000) operand |= 06000; break;
|
|
|
|
case 'g': if ( mode & 02000) operand |= 06000; break;
|
|
|
|
};
|
|
|
|
who_mask &= ~((mode_t) 01000);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
bool unknown = false;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
switch ( str[index] )
|
|
|
|
{
|
|
|
|
case 'r': operand |= 00444; break;
|
|
|
|
case 'w': operand |= 00222; break;
|
|
|
|
case 'x': operand |= 00111; break;
|
|
|
|
case 'X':
|
|
|
|
if ( S_ISDIR(type) || (mode & 0111) )
|
|
|
|
operand |= 00111;
|
|
|
|
break;
|
|
|
|
case 's': operand |= 06000; break;
|
|
|
|
case 't': operand |= 00000; break;
|
|
|
|
default: unknown = true; break;
|
|
|
|
}
|
|
|
|
} while ( !unknown && (index++, true) );
|
|
|
|
}
|
|
|
|
switch ( op )
|
|
|
|
{
|
|
|
|
case '+': mode |= (operand & who_mask); break;
|
|
|
|
case '-': mode &= ~(operand & who_mask); break;
|
|
|
|
case '=': mode = (mode & ~who_mask) | (operand & who_mask); break;
|
|
|
|
default: __builtin_unreachable();
|
|
|
|
}
|
|
|
|
} while ( str[index] == '+' ||
|
|
|
|
str[index] == '-' ||
|
|
|
|
str[index] == '=' );
|
|
|
|
} while ( str[index] == ',' && (index++, true) );
|
|
|
|
if ( str[index] )
|
|
|
|
return (mode_t) -1;
|
|
|
|
return mode;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool is_valid_modespec(const char* str)
|
|
|
|
{
|
|
|
|
return execute_modespec(str, 0, 0, 0) != (mode_t) -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool do_chmod_directory(int fd,
|
|
|
|
const char* path,
|
|
|
|
const char* modespec,
|
|
|
|
int flags,
|
|
|
|
enum symderef symderef);
|
|
|
|
|
|
|
|
static bool do_chmod(int dirfd,
|
|
|
|
const char* relpath,
|
|
|
|
const char* path,
|
|
|
|
const char* modespec,
|
|
|
|
int flags,
|
|
|
|
enum symderef symderef)
|
|
|
|
{
|
|
|
|
bool success = true;
|
|
|
|
|
|
|
|
int fd_open_flags = O_RDONLY;
|
|
|
|
if ( symderef == SYMDEREF_NONE )
|
|
|
|
fd_open_flags |= O_SYMLINK_NOFOLLOW;
|
|
|
|
int fd = openat(dirfd, relpath, fd_open_flags);
|
|
|
|
if ( fd < 0 )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("%s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct stat st;
|
|
|
|
if ( fstat(fd, &st) < 0 )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("stat: %s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
close(fd);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( S_ISLNK(st.st_mode) )
|
|
|
|
{
|
|
|
|
close(fd);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
mode_t old_mode = st.st_mode & 07777;
|
|
|
|
mode_t new_mode = execute_modespec(modespec, old_mode, st.st_mode, getumask());
|
|
|
|
assert(new_mode != (mode_t) -1);
|
|
|
|
|
|
|
|
if ( fchmod(fd, new_mode) < 0 )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("changing permissions: %s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
success = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
mode_t naive_mode = execute_modespec(modespec, old_mode, st.st_mode, 0);
|
|
|
|
assert(naive_mode != (mode_t) -1);
|
|
|
|
|
|
|
|
if ( new_mode & ~naive_mode )
|
2020-10-17 11:31:22 -04:00
|
|
|
warnx("%s: new permissions are %jo, not %jo",
|
|
|
|
path, (uintmax_t) new_mode, (uintmax_t) naive_mode);
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
if ( (flags & FLAG_VERBOSE) ||
|
|
|
|
(old_mode != new_mode && (flags & FLAG_CHANGES)) )
|
|
|
|
{
|
|
|
|
if ( old_mode == new_mode )
|
|
|
|
printf("mode of `%s' retained as %jo\n",
|
|
|
|
path, (uintmax_t) new_mode);
|
|
|
|
else
|
|
|
|
printf("mode of `%s' changed from %jo to %jo\n",
|
|
|
|
path, (uintmax_t) old_mode, (uintmax_t) new_mode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( S_ISDIR(st.st_mode) && (flags & FLAG_RECURSIVE) )
|
|
|
|
{
|
|
|
|
if ( !do_chmod_directory(fd, path, modespec, flags, symderef) )
|
|
|
|
success = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
close(fd);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool do_chmod_directory(int fd,
|
|
|
|
const char* path,
|
|
|
|
const char* modespec,
|
|
|
|
int flags,
|
|
|
|
enum symderef symderef)
|
|
|
|
{
|
|
|
|
if ( symderef == SYMDEREF_ARGUMENTS )
|
|
|
|
symderef = SYMDEREF_NONE;
|
|
|
|
|
|
|
|
int fd_copy = dup(fd);
|
|
|
|
if ( fd_copy < 0 )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("dup: %s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
DIR* dir = fdopendir(fd_copy);
|
|
|
|
if ( !dir )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("fdopendir: %s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
close(fd_copy);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char* joiner = "/";
|
|
|
|
size_t path_length = strlen(path);
|
|
|
|
if ( path_length && path[path_length - 1] == '/' )
|
|
|
|
joiner = "";
|
|
|
|
|
|
|
|
bool success = true;
|
2015-11-19 20:57:09 -05:00
|
|
|
struct dirent* entry;
|
|
|
|
while ( (errno = 0, entry = readdir(dir)) )
|
2013-06-18 07:40:12 -04:00
|
|
|
{
|
|
|
|
if ( !strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..") )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
char* entry_path;
|
|
|
|
if ( asprintf(&entry_path, "%s%s%s", path, joiner, entry->d_name) < 0 )
|
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("asprintf: `%s%s%s'", path, joiner, entry->d_name);
|
2013-06-18 07:40:12 -04:00
|
|
|
success = false;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !do_chmod(fd, entry->d_name, entry_path, modespec, flags, symderef) )
|
|
|
|
success = false;
|
|
|
|
|
|
|
|
free(entry_path);
|
|
|
|
}
|
|
|
|
|
2015-11-19 20:57:09 -05:00
|
|
|
if ( errno != 0 )
|
2013-06-18 07:40:12 -04:00
|
|
|
{
|
2020-10-17 11:31:22 -04:00
|
|
|
warn("reading directory: %s", path);
|
2013-06-18 07:40:12 -04:00
|
|
|
closedir(dir);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
closedir(dir);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool is_ambiguous_option(const char* str)
|
|
|
|
{
|
|
|
|
if ( str[0] != '-' )
|
|
|
|
return false;
|
|
|
|
if ( str[1] == '-' )
|
|
|
|
return false;
|
|
|
|
return is_valid_modespec(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void compact_arguments(int* argc, char*** argv)
|
|
|
|
{
|
|
|
|
for ( int i = 0; i < *argc; i++ )
|
|
|
|
{
|
|
|
|
while ( i < *argc && !(*argv)[i] )
|
|
|
|
{
|
|
|
|
for ( int n = i; n < *argc; n++ )
|
|
|
|
(*argv)[n] = (*argv)[n+1];
|
|
|
|
(*argc)--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int main(int argc, char* argv[])
|
|
|
|
{
|
|
|
|
int flags = 0;
|
|
|
|
enum symderef symderef = SYMDEREF_DEFAULT;
|
|
|
|
|
|
|
|
for ( int i = 1; i < argc; i++ )
|
|
|
|
{
|
|
|
|
const char* arg = argv[i];
|
|
|
|
if ( arg[0] != '-' || !arg[1] )
|
|
|
|
continue;
|
|
|
|
if ( is_ambiguous_option(arg) )
|
|
|
|
continue;
|
|
|
|
argv[i] = NULL;
|
2020-10-17 11:31:22 -04:00
|
|
|
if ( !strcmp(arg, "--") )
|
|
|
|
break;
|
2013-06-18 07:40:12 -04:00
|
|
|
if ( arg[1] != '-' )
|
|
|
|
{
|
2016-02-28 18:40:20 -05:00
|
|
|
char c;
|
|
|
|
while ( (c = *++arg) ) switch ( c )
|
2013-06-18 07:40:12 -04:00
|
|
|
{
|
|
|
|
case 'c': flags |= FLAG_CHANGES; break;
|
2020-10-17 11:31:22 -04:00
|
|
|
case 'h': flags |= FLAG_NOFOLLOW; break;
|
2013-06-18 07:40:12 -04:00
|
|
|
case 'H': symderef = SYMDEREF_ARGUMENTS; break;
|
|
|
|
case 'L': symderef = SYMDEREF_ALWAYS; break;
|
|
|
|
case 'P': symderef = SYMDEREF_NONE; break;
|
|
|
|
case 'R': flags |= FLAG_RECURSIVE; break;
|
|
|
|
case 'v': flags |= FLAG_VERBOSE; break;
|
|
|
|
default:
|
2020-10-17 11:31:22 -04:00
|
|
|
errx(1, "unknown option -- '%c'", c);
|
2013-06-18 07:40:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else if ( !strcmp(arg, "--changes") )
|
|
|
|
flags |= FLAG_CHANGES;
|
|
|
|
else if ( !strcmp(arg, "--verbose") )
|
|
|
|
flags |= FLAG_VERBOSE;
|
|
|
|
else if ( !strcmp(arg, "--recursive") )
|
|
|
|
flags |= FLAG_RECURSIVE;
|
|
|
|
else
|
2020-10-17 11:31:22 -04:00
|
|
|
errx(1, "unknown option: %s", arg);
|
2013-06-18 07:40:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
compact_arguments(&argc, &argv);
|
|
|
|
|
2020-10-17 11:31:22 -04:00
|
|
|
if ( (flags & FLAG_RECURSIVE) && (flags & FLAG_NOFOLLOW) )
|
|
|
|
errx(1, "the -R and -h options are mutually incompatible");
|
|
|
|
|
2013-06-18 07:40:12 -04:00
|
|
|
if ( flags & FLAG_RECURSIVE )
|
|
|
|
{
|
|
|
|
if ( symderef == SYMDEREF_DEFAULT )
|
|
|
|
symderef = SYMDEREF_NONE;
|
|
|
|
}
|
|
|
|
else
|
2020-10-17 11:31:22 -04:00
|
|
|
symderef = flags & FLAG_NOFOLLOW ? SYMDEREF_NONE : SYMDEREF_ALWAYS;
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
if ( argc == 1 )
|
2020-10-17 11:31:22 -04:00
|
|
|
errx(1, "missing operand");
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
const char* modespec = argv[1];
|
|
|
|
|
|
|
|
if ( !is_valid_modespec(modespec) )
|
2020-10-17 11:31:22 -04:00
|
|
|
errx(1, "invalid mode: `%s'", modespec);
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
if ( argc == 2 )
|
2020-10-17 11:31:22 -04:00
|
|
|
errx(1, "missing operand after `%s'", modespec);
|
2013-06-18 07:40:12 -04:00
|
|
|
|
|
|
|
bool success = true;
|
|
|
|
for ( int i = 2; i < argc; i++ )
|
|
|
|
{
|
|
|
|
const char* arg = argv[i];
|
|
|
|
if ( !do_chmod(AT_FDCWD, arg, arg, modespec, flags, symderef) )
|
|
|
|
success = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return success ? 0 : 1;
|
|
|
|
}
|