mirror of
https://git.openldap.org/openldap/openldap.git
synced 2024-12-15 03:01:09 +08:00
b5494457d8
This could cause problems on odd systems. The generic headers should be extended as needed to include necessary system headers or, if necessary, make explicit declarations. Extended ac/string.h header to look for string.h/strings.h if STDC_HEADERS is not defined. Also provide basic declarations for str*() functions. This could cause problems on odd systems. Extended ac/unistd.h header to define basic declaration for misc functions that might be missing from headers. This includes externs for getenv(), getopt(), mktemp(), tempname(). Protect fax500.h from multiple inclusion. Moved includes of system/generic headers back to source files. Made mail500 helper functions static. Fixed includes of ctype.h, signal.h, etc. to use generics. lutil/tempname.c: was including stdlib.h twice, one should stdio.h. Wrapped <sys/resource.h> with HAVE_SYS_RESOURCE_H. lber/io.c/ber_get_next(): Changed noctets back to signed. Used with BerRead which expects signed int as second arg and returns signed int.
637 lines
15 KiB
C
637 lines
15 KiB
C
/*
|
||
* Copyright (c) 1991, 1993
|
||
* Regents of the University of Michigan. All rights reserved.
|
||
*
|
||
* Redistribution and use in source and binary forms are permitted
|
||
* provided that this notice is preserved and that due credit is given
|
||
* to the University of Michigan at Ann Arbor. The name of the University
|
||
* may not be used to endorse or promote products derived from this
|
||
* software without specific prior written permission. This software
|
||
* is provided ``as is'' without express or implied warranty.
|
||
*/
|
||
|
||
#include "portable.h"
|
||
|
||
#include <stdio.h>
|
||
|
||
#include <ac/ctype.h>
|
||
#include <ac/string.h>
|
||
#include <ac/time.h>
|
||
|
||
#include <lber.h>
|
||
#include <ldap.h>
|
||
|
||
#include "ud.h"
|
||
|
||
struct entry Entry;
|
||
|
||
static char *time2text(char *ldtimestr, int dateonly);
|
||
static long gtime(struct tm *tm);
|
||
|
||
/*
|
||
* When displaying entries, display only these attributes, and in this
|
||
* order.
|
||
*/
|
||
static char *person_attr_print_order[] = {
|
||
"cn",
|
||
"mail",
|
||
"telephoneNumber",
|
||
"facsimileTelephoneNumber",
|
||
"pager",
|
||
"postalAddress",
|
||
"title",
|
||
"uid",
|
||
"multiLineDescription",
|
||
"homePhone",
|
||
"homePostalAddress",
|
||
"drink",
|
||
"labeledURL",
|
||
"onVacation",
|
||
"vacationMessage",
|
||
"memberOfGroup",
|
||
"lastModifiedBy",
|
||
"lastModifiedTime",
|
||
NULL
|
||
};
|
||
|
||
static char *group_attr_print_order[] = {
|
||
"cn",
|
||
"facsimileTelephoneNumber",
|
||
"telephoneNumber",
|
||
"postalAddress",
|
||
"multiLineDescription",
|
||
"joinable",
|
||
"associatedDomain",
|
||
"owner",
|
||
"moderator",
|
||
"ErrorsTo",
|
||
"rfc822ErrorsTo",
|
||
"RequestsTo",
|
||
"rfc822RequestsTo",
|
||
"member",
|
||
"mail",
|
||
"labeledURL",
|
||
"lastModifiedBy",
|
||
"lastModifiedTime",
|
||
NULL
|
||
};
|
||
|
||
|
||
void
|
||
parse_answer( LDAPMessage *s )
|
||
{
|
||
int idx;
|
||
char **rdns;
|
||
BerElement *cookie;
|
||
register LDAPMessage *ep;
|
||
register char *ap;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->parse_answer(%x)\n", s);
|
||
#endif
|
||
|
||
clear_entry();
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" Done clearing entry\n");
|
||
#endif
|
||
for (ep = ldap_first_entry(ld, s); ep != NULL; ep = ldap_next_entry(ld, ep)) {
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" Determining DN and name\n");
|
||
#endif
|
||
Entry.DN = ldap_get_dn(ld, ep);
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" DN = %s\n", Entry.DN);
|
||
#endif
|
||
rdns = ldap_explode_dn(Entry.DN, TRUE);
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" Name = %s\n", *rdns);
|
||
#endif
|
||
Entry.name = strdup(*rdns);
|
||
ldap_value_free(rdns);
|
||
for (ap = ldap_first_attribute(ld, ep, &cookie); ap != NULL; ap = ldap_next_attribute(ld, ep, cookie)) {
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf("parsing ap = %s\n", ap);
|
||
#endif
|
||
if ((idx = attr_to_index(ap)) < 0) {
|
||
printf(" Unknown attribute \"%s\"\n", ap);
|
||
continue;
|
||
}
|
||
add_value(&(Entry.attrs[idx]), ep, ap);
|
||
}
|
||
}
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" Done parsing entry\n");
|
||
#endif
|
||
}
|
||
|
||
void
|
||
add_value( struct attribute *attr, LDAPMessage *ep, char *ap )
|
||
{
|
||
register int i = 0;
|
||
char **vp, **tp, **avp;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->add_value(%x, %x, %s)\n", attr, ep, ap);
|
||
#endif
|
||
vp = (char **) ldap_get_values(ld, ep, ap);
|
||
|
||
/*
|
||
* Fill in the attribute structure for this attribute. This
|
||
* stores away the values (using strdup()) and the count. Terminate
|
||
* the list with a NULL pointer.
|
||
*
|
||
* attr->quipu_name has already been set during initialization.
|
||
*/
|
||
if ((attr->number_of_values = ldap_count_values(vp)) > 0) {
|
||
attr->values = (char **) Malloc((unsigned) ((attr->number_of_values + 1) * sizeof(char *)));
|
||
avp = attr->values;
|
||
|
||
for (i = 1, tp = vp; *tp != NULL; i++, tp++) {
|
||
#ifdef DEBUG
|
||
if (debug & D_PARSE)
|
||
printf(" value #%d %s\n", i, *tp);
|
||
#endif
|
||
/*
|
||
* The 'name' field of the Entry structure already has
|
||
* has the first part of the DN copied into it. Thus,
|
||
* we don't need to save it away here again. Also, by
|
||
* tossing it away here, we make printing this info out
|
||
* a bit easier later.
|
||
*/
|
||
if (!strcmp(ap, "cn") && !strcmp(*tp, Entry.name)) {
|
||
attr->number_of_values--;
|
||
continue;
|
||
}
|
||
*avp++ = strdup(*tp);
|
||
}
|
||
*avp = NULL;
|
||
}
|
||
ldap_value_free(vp);
|
||
}
|
||
|
||
void
|
||
print_an_entry( void )
|
||
{
|
||
int n = 0, i, idx;
|
||
char is_a_group, **order;
|
||
char *sub_list[MAX_VALUES], buf[SMALL_BUF_SIZE];
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->print_an_entry()\n");
|
||
#endif
|
||
printf(" \"%s\"\n", Entry.name);
|
||
|
||
/*
|
||
* If the entry is a group, find all of the subscribers to that
|
||
* group. A subscriber is an entry that *points* to a group entry,
|
||
* and a member is an entry that is included as part of a group
|
||
* entry.
|
||
*
|
||
* We also need to select the appropriate output format here.
|
||
*/
|
||
is_a_group = isgroup();
|
||
if (is_a_group) {
|
||
order = (char **) group_attr_print_order;
|
||
n = find_all_subscribers(sub_list, Entry.DN);
|
||
#ifdef DEBUG
|
||
if (debug & D_PRINT)
|
||
printf(" Group \"%s\" has %d subscribers\n",
|
||
Entry.name, n);
|
||
#endif
|
||
}
|
||
else
|
||
order = (char **) person_attr_print_order;
|
||
|
||
for (i = 0; order[i] != NULL; i++) {
|
||
idx = attr_to_index(order[i]);
|
||
#ifdef DEBUG
|
||
if (debug & D_PRINT) {
|
||
printf(" ATTR #%2d = %s [%s] (%d values)\n", i + 1,
|
||
Entry.attrs[idx].output_string,
|
||
Entry.attrs[idx].quipu_name,
|
||
Entry.attrs[idx].number_of_values);
|
||
}
|
||
#endif
|
||
if (idx < 0)
|
||
continue;
|
||
if (Entry.attrs[idx].number_of_values == 0)
|
||
continue;
|
||
if (isadn(order[i]))
|
||
print_DN(Entry.attrs[idx]);
|
||
else if (isaurl(order[i]))
|
||
print_URL(Entry.attrs[idx]);
|
||
else if (isadate(order[i])) {
|
||
/* fix time and date, then call usual routine */
|
||
Entry.attrs[idx].values[0] =
|
||
time2text(Entry.attrs[idx].values[0], FALSE);
|
||
print_values(Entry.attrs[idx]);
|
||
}
|
||
else
|
||
print_values(Entry.attrs[idx]);
|
||
}
|
||
|
||
/*
|
||
* If it is a group, then we should print the subscriber list (if
|
||
* there are any). If there are a lot of them, prompt the user
|
||
* before printing them.
|
||
*/
|
||
if (is_a_group && (n > 0)) {
|
||
char *label = "Subscribers: ";
|
||
|
||
if (n > TOO_MANY_TO_PRINT) {
|
||
printf(" There are %d subscribers. Print them? ", n);
|
||
fflush(stdout);
|
||
fetch_buffer(buf, sizeof(buf), stdin);
|
||
if (!((buf[0] == 'y') || (buf[0] == 'Y')))
|
||
return;
|
||
}
|
||
format2((char *) my_ldap_dn2ufn(sub_list[n - 1]), label, (char *) NULL, 2,
|
||
2 + strlen(label) + 1, col_size);
|
||
for (n--; n > 0; n--)
|
||
format2((char *) my_ldap_dn2ufn(sub_list[n - 1]), (char *) NULL,
|
||
(char *) NULL, 2 + strlen(label),
|
||
2 + strlen(label) + 2, col_size);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
#define OUT_LABEL_LEN 20
|
||
|
||
/* prints the values associated with an attribute */
|
||
void
|
||
print_values( struct attribute A )
|
||
{
|
||
register int i, k;
|
||
register char *cp, **vp;
|
||
char out_buf[MED_BUF_SIZE], *padding = NULL;
|
||
int lead;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->print_values(%x)\n", A);
|
||
#endif
|
||
if (A.number_of_values == 0)
|
||
return;
|
||
if ((vp = A.values) == NULL)
|
||
return;
|
||
|
||
/*
|
||
* Pad out the output string label so that it fills the
|
||
* whole field of length OUT_LABEL_LEN.
|
||
*/
|
||
out_buf[0] = '\0';
|
||
i = OUT_LABEL_LEN - strlen(A.output_string);
|
||
if (i < 0) {
|
||
printf("Output string for \"%s\" is too long. Maximum length is %d characters\n", A.quipu_name, OUT_LABEL_LEN);
|
||
return;
|
||
}
|
||
if (isgroup() && !strcmp(A.quipu_name, "mail") && (Entry.attrs[attr_to_index("member")].number_of_values == 0)) {
|
||
A.output_string = "Members";
|
||
i = OUT_LABEL_LEN - strlen(A.output_string);
|
||
padding = (char *) Malloc((unsigned) (i + 1));
|
||
(void) memset(padding, ' ', i);
|
||
*(padding + i) = '\0';
|
||
sprintf(out_buf, "%s:%s", A.output_string, padding);
|
||
}
|
||
else if (!(isgroup() && !strcmp(A.quipu_name, "mail") && (Entry.attrs[attr_to_index("member")].number_of_values > 0))) {
|
||
padding = (char *) Malloc((unsigned) (i + 1));
|
||
(void) memset(padding, ' ', i);
|
||
*(padding + i) = '\0';
|
||
sprintf(out_buf, "%s:%s", A.output_string, padding);
|
||
}
|
||
/*
|
||
* If this happens to be a group, then do not print the output
|
||
* string if we have already printed out some members.
|
||
*/
|
||
else if (isgroup() && !strcmp(A.quipu_name, "mail") && (Entry.attrs[attr_to_index("member")].number_of_values > 0)) {
|
||
padding = (char *) Malloc((unsigned) (OUT_LABEL_LEN + 2));
|
||
(void) memset(padding, ' ', OUT_LABEL_LEN + 1);
|
||
*(padding + OUT_LABEL_LEN + 1) = '\0';
|
||
sprintf(out_buf, "%s", padding);
|
||
}
|
||
lead = strlen(out_buf) + 2;
|
||
|
||
printf(" %s", out_buf);
|
||
for (i = 0; *vp != NULL; i++, vp++) {
|
||
if (i > 0) {
|
||
if (!strncmp(A.output_string, "Home a", 6) || !strncmp(A.output_string, "Business a", 10)) {
|
||
printf(" %s", out_buf);
|
||
}
|
||
else {
|
||
for (k = lead; k > 0; k--)
|
||
putchar(' ');
|
||
}
|
||
}
|
||
for (cp = *vp; *cp != '\0'; cp++) {
|
||
switch (*cp) {
|
||
case '$' :
|
||
if (!strncmp(A.output_string, "Home a", 6) || !strncmp(A.output_string, "Business a", 10) || !strcmp(A.quipu_name, "multiLineDescription")) {
|
||
putchar('\n');
|
||
for (k = lead; k > 0; k--)
|
||
putchar(' ');
|
||
while (isspace(*(cp + 1)))
|
||
cp++;
|
||
}
|
||
else
|
||
putchar(*cp);
|
||
break;
|
||
case '\n' :
|
||
putchar('%');
|
||
putchar('\n');
|
||
break;
|
||
default:
|
||
putchar(*cp);
|
||
}
|
||
}
|
||
putchar('\n');
|
||
}
|
||
if (padding != NULL)
|
||
Free(padding);
|
||
return;
|
||
}
|
||
|
||
/* prints the DN's associated with an attribute */
|
||
void
|
||
print_DN( struct attribute A )
|
||
{
|
||
int i, lead;
|
||
register char **vp;
|
||
char out_buf[MED_BUF_SIZE], *padding = NULL;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->print_DN(%x)\n", A);
|
||
#endif
|
||
if (A.number_of_values == 0)
|
||
return;
|
||
/*
|
||
* Pad out the output string label so that it fills the
|
||
* whole field of length OUT_LABEL_LEN.
|
||
*/
|
||
i = OUT_LABEL_LEN - strlen(A.output_string);
|
||
if (i > 0) {
|
||
padding = (char *) Malloc((unsigned) (i + 1));
|
||
(void) memset(padding, ' ', i);
|
||
*(padding + i) = '\0';
|
||
sprintf(out_buf, "%s:%s", A.output_string, padding);
|
||
(void) Free(padding);
|
||
}
|
||
lead = strlen(out_buf) + 2;
|
||
|
||
vp = A.values;
|
||
format2((char *) my_ldap_dn2ufn(*vp), out_buf, (char *) NULL, 2, lead + 1, col_size);
|
||
for (vp++; *vp != NULL; vp++) {
|
||
format2((char *) my_ldap_dn2ufn(*vp), (char *) NULL, (char *) NULL, lead,
|
||
lead + 1, col_size);
|
||
}
|
||
return;
|
||
}
|
||
|
||
void
|
||
clear_entry( void )
|
||
{
|
||
register int i;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->clear_entry()\n");
|
||
if ((debug & D_PRINT) && (Entry.name != NULL))
|
||
printf(" Clearing entry \"%s\"\n", Entry.name);
|
||
#endif
|
||
if (Entry.DN != NULL)
|
||
Free(Entry.DN);
|
||
if (Entry.name != NULL)
|
||
Free(Entry.name);
|
||
Entry.may_join = FALSE;
|
||
Entry.subscriber_count = -1;
|
||
Entry.DN = Entry.name = NULL;
|
||
|
||
/* clear all of the values associated with all attributes */
|
||
for (i = 0; attrlist[i].quipu_name != NULL; i++) {
|
||
#ifdef DEBUG
|
||
if (debug & D_PRINT)
|
||
printf(" Clearing attribute \"%s\" -- ",
|
||
Entry.attrs[i].quipu_name);
|
||
#endif
|
||
if (Entry.attrs[i].values == NULL) {
|
||
#ifdef DEBUG
|
||
if (debug & D_PRINT)
|
||
printf(" no values, skipping\n");
|
||
#endif
|
||
continue;
|
||
}
|
||
#ifdef DEBUG
|
||
if (debug & D_PRINT)
|
||
printf(" freeing %d values\n",
|
||
Entry.attrs[i].number_of_values);
|
||
#endif
|
||
Entry.attrs[i].number_of_values = 0;
|
||
ldap_value_free(Entry.attrs[i].values);
|
||
Entry.attrs[i].values = (char **) NULL;
|
||
|
||
/*
|
||
* Note: We do not clear either of the char * fields
|
||
* since they will always be applicable.
|
||
*/
|
||
}
|
||
}
|
||
|
||
int
|
||
attr_to_index( char *s )
|
||
{
|
||
register int i;
|
||
|
||
for (i = 0; attrlist[i].quipu_name != NULL; i++)
|
||
if (!strcasecmp(s, attrlist[i].quipu_name))
|
||
return(i);
|
||
return(-1);
|
||
}
|
||
|
||
void
|
||
initialize_attribute_strings( void )
|
||
{
|
||
register int i;
|
||
|
||
for (i = 0; attrlist[i].quipu_name != NULL; i++)
|
||
Entry.attrs[i].quipu_name = attrlist[i].quipu_name;
|
||
for (i = 0; attrlist[i].quipu_name != NULL; i++)
|
||
Entry.attrs[i].output_string = attrlist[i].output_string;
|
||
}
|
||
|
||
/* prints the URL/label pairs associated with an attribute */
|
||
void
|
||
print_URL( struct attribute A )
|
||
{
|
||
int i, lead;
|
||
register char **vp;
|
||
char out_buf[MED_BUF_SIZE], *padding = NULL;
|
||
|
||
#ifdef DEBUG
|
||
if (debug & D_TRACE)
|
||
printf("->print_URL(%x)\n", A);
|
||
#endif
|
||
if (A.number_of_values == 0)
|
||
return;
|
||
/*
|
||
* Pad out the output string label so that it fills the
|
||
* whole field of length OUT_LABEL_LEN.
|
||
*/
|
||
i = OUT_LABEL_LEN - strlen(A.output_string);
|
||
if (i > 0) {
|
||
padding = (char *) Malloc((unsigned) (i + 1));
|
||
(void) memset(padding, ' ', i);
|
||
*(padding + i) = '\0';
|
||
sprintf(out_buf, "%s:%s", A.output_string, padding);
|
||
}
|
||
lead = strlen(out_buf) + 2;
|
||
|
||
vp = A.values;
|
||
print_one_URL(*vp, 2, out_buf, lead);
|
||
for (vp++; *vp != NULL; vp++)
|
||
print_one_URL(*vp, lead, (char *) NULL, lead);
|
||
if (padding != NULL)
|
||
Free(padding);
|
||
return;
|
||
}
|
||
|
||
void
|
||
print_one_URL( char *s, int label_lead, char *tag, int url_lead )
|
||
{
|
||
register int i;
|
||
char c, *cp, *url;
|
||
|
||
for (cp = s; !isspace(*cp) && (*cp != '\0'); cp++)
|
||
;
|
||
c = *cp;
|
||
*cp = '\0';
|
||
url = strdup(s);
|
||
*cp = c;
|
||
if (*cp != '\0') {
|
||
for (cp++; isspace(*cp); cp++)
|
||
;
|
||
}
|
||
else
|
||
cp = "(no description available)";
|
||
format2(cp, tag, (char *) NULL, label_lead, label_lead + 1, col_size);
|
||
for (i = url_lead + 2; i > 0; i--)
|
||
printf(" ");
|
||
printf("%s\n", url);
|
||
Free(url);
|
||
}
|
||
|
||
|
||
#define GET2BYTENUM( p ) (( *p - '0' ) * 10 + ( *(p+1) - '0' ))
|
||
|
||
static char *
|
||
time2text( char *ldtimestr, int dateonly )
|
||
{
|
||
struct tm t;
|
||
char *p, *timestr, zone, *fmterr = "badly formatted time";
|
||
time_t gmttime;
|
||
|
||
memset( (char *)&t, 0, sizeof( struct tm ));
|
||
if ( strlen( ldtimestr ) < 13 ) {
|
||
return( fmterr );
|
||
}
|
||
|
||
for ( p = ldtimestr; p - ldtimestr < 12; ++p ) {
|
||
if ( !isdigit( *p )) {
|
||
return( fmterr );
|
||
}
|
||
}
|
||
|
||
p = ldtimestr;
|
||
t.tm_year = GET2BYTENUM( p ); p += 2;
|
||
t.tm_mon = GET2BYTENUM( p ) - 1; p += 2;
|
||
t.tm_mday = GET2BYTENUM( p ); p += 2;
|
||
t.tm_hour = GET2BYTENUM( p ); p += 2;
|
||
t.tm_min = GET2BYTENUM( p ); p += 2;
|
||
t.tm_sec = GET2BYTENUM( p ); p += 2;
|
||
|
||
if (( zone = *p ) == 'Z' ) { /* GMT */
|
||
zone = '\0'; /* no need to indicate on screen, so we make it null */
|
||
}
|
||
|
||
gmttime = gtime( &t );
|
||
timestr = ctime( &gmttime );
|
||
|
||
timestr[ strlen( timestr ) - 1 ] = zone; /* replace trailing newline */
|
||
if ( dateonly ) {
|
||
strcpy( timestr + 11, timestr + 20 );
|
||
}
|
||
|
||
Free ( ldtimestr );
|
||
return( strdup( timestr ) );
|
||
}
|
||
|
||
|
||
/* gtime.c - inverse gmtime */
|
||
|
||
#include <ac/time.h>
|
||
|
||
/* gtime(): the inverse of localtime().
|
||
This routine was supplied by Mike Accetta at CMU many years ago.
|
||
*/
|
||
|
||
int dmsize[] = {
|
||
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||
};
|
||
|
||
#define dysize(y) \
|
||
(((y) % 4) ? 365 : (((y) % 100) ? 366 : (((y) % 400) ? 365 : 366)))
|
||
|
||
#define YEAR(y) ((y) >= 100 ? (y) : (y) + 1900)
|
||
|
||
/* */
|
||
|
||
static long
|
||
gtime( struct tm *tm )
|
||
{
|
||
register int i,
|
||
sec,
|
||
mins,
|
||
hour,
|
||
mday,
|
||
mon,
|
||
year;
|
||
register long result;
|
||
|
||
if ((sec = tm -> tm_sec) < 0 || sec > 59
|
||
|| (mins = tm -> tm_min) < 0 || mins > 59
|
||
|| (hour = tm -> tm_hour) < 0 || hour > 24
|
||
|| (mday = tm -> tm_mday) < 1 || mday > 31
|
||
|| (mon = tm -> tm_mon + 1) < 1 || mon > 12)
|
||
return ((long) -1);
|
||
if (hour == 24) {
|
||
hour = 0;
|
||
mday++;
|
||
}
|
||
year = YEAR (tm -> tm_year);
|
||
|
||
result = 0L;
|
||
for (i = 1970; i < year; i++)
|
||
result += dysize (i);
|
||
if (dysize (year) == 366 && mon >= 3)
|
||
result++;
|
||
while (--mon)
|
||
result += dmsize[mon - 1];
|
||
result += mday - 1;
|
||
result = 24 * result + hour;
|
||
result = 60 * result + mins;
|
||
result = 60 * result + sec;
|
||
|
||
return result;
|
||
}
|