mirror of
https://github.com/libtom/libtomcrypt
synced 2026-08-25 20:26:07 -04:00
Put realloc logic into pem_read()
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
This commit is contained in:
parent
def00a6b11
commit
6ae30f2660
5 changed files with 183 additions and 108 deletions
|
|
@ -382,12 +382,23 @@ struct bufp {
|
|||
};
|
||||
|
||||
#define SET_BUFP(n, d, l) n.start = (char*)d, n.work = (char*)d, n.end = (char*)d + l + 1
|
||||
#define UPDATE_BUFP(n, d, w, l) n.start = (char*)d, n.work = (char*)d + w, n.end = (char*)d + l + 1
|
||||
|
||||
struct get_char;
|
||||
struct get_char_api {
|
||||
int (*get)(struct get_char*);
|
||||
int (*reset)(struct get_char*);
|
||||
unsigned long (*len)(struct get_char*);
|
||||
};
|
||||
|
||||
struct get_char {
|
||||
int (*get)(struct get_char*);
|
||||
struct get_char_api api;
|
||||
union {
|
||||
#ifndef LTC_NO_FILE
|
||||
FILE *f;
|
||||
struct {
|
||||
FILE *f;
|
||||
long original_pos;
|
||||
} f;
|
||||
#endif /* LTC_NO_FILE */
|
||||
struct bufp buf;
|
||||
} data;
|
||||
|
|
@ -395,6 +406,17 @@ struct get_char {
|
|||
char unget_buf_[LTC_PEM_DECODE_BUFSZ];
|
||||
int prev_get;
|
||||
};
|
||||
|
||||
#define pem_get_char_init(b, l) { \
|
||||
.api = get_char_buffer_api, \
|
||||
SET_BUFP(.data.buf, (b), (l)) \
|
||||
}
|
||||
|
||||
#define pem_get_char_init_filehandle(fi) { \
|
||||
.api = get_char_filehandle_api, \
|
||||
.data.f.f = (fi), \
|
||||
.data.f.original_pos = ftell(fi) \
|
||||
}
|
||||
#endif
|
||||
|
||||
/* others */
|
||||
|
|
@ -417,10 +439,10 @@ int pem_decrypt(unsigned char *data, unsigned long *datalen,
|
|||
const struct blockcipher_info *info,
|
||||
enum padding_type padding);
|
||||
#ifndef LTC_NO_FILE
|
||||
int pem_get_char_from_file(struct get_char *g);
|
||||
extern const struct get_char_api get_char_filehandle_api;
|
||||
#endif /* LTC_NO_FILE */
|
||||
int pem_get_char_from_buf(struct get_char *g);
|
||||
int pem_read(void *asn1_cert, unsigned long *asn1_len, struct pem_headers *hdr, struct get_char *g);
|
||||
extern const struct get_char_api get_char_buffer_api;
|
||||
int pem_read(void **dest, unsigned long *len, struct pem_headers *hdr, struct get_char *g);
|
||||
#endif
|
||||
|
||||
/* tomcrypt_pk.h */
|
||||
|
|
@ -709,7 +731,7 @@ int x509_decode_subject_public_key_info(const unsigned char *in, unsigned long i
|
|||
|
||||
int x509_get_pka(const ltc_asn1_list *pub, enum ltc_pka_id *pka);
|
||||
int x509_get_sig_alg(const ltc_asn1_list *pub, ltc_x509_signature_algorithm *sig_alg);
|
||||
int x509_import_spki(const unsigned char *asn1_cert, unsigned long asn1_len, ltc_pka_key *k, ltc_asn1_list **root);
|
||||
int x509_import_spki(const unsigned char *buf, unsigned long len, ltc_pka_key *k, ltc_asn1_list **root);
|
||||
int x509_get_extensions(const ltc_asn1_list *seq, ltc_x509_extensions *extensions);
|
||||
void x509_free_extensions(const ltc_x509_extensions *extensions);
|
||||
int x509_get_serial(const ltc_asn1_list *asn1, ltc_x509_string *serial);
|
||||
|
|
|
|||
|
|
@ -127,21 +127,16 @@ static const import_fn s_import_openssl_fns[LTC_PKA_NUM] = {
|
|||
static int s_decode(struct get_char *g, ltc_pka_key *k, const password_ctx *pw_ctx)
|
||||
{
|
||||
unsigned char *asn1_cert = NULL;
|
||||
unsigned long w, asn1_len, n;
|
||||
unsigned long w = 0, asn1_len, n;
|
||||
int err = CRYPT_ERROR;
|
||||
struct pem_headers hdr = { 0 };
|
||||
struct password pw = { 0 };
|
||||
enum ltc_pka_id pka;
|
||||
XMEMSET(k, 0, sizeof(*k));
|
||||
w = LTC_PEM_READ_BUFSIZE * 2;
|
||||
retry:
|
||||
asn1_cert = XREALLOC(asn1_cert, w);
|
||||
for (n = 0; n < pem_std_headers_num; ++n) {
|
||||
hdr.id = &pem_std_headers[n];
|
||||
err = pem_read(asn1_cert, &w, &hdr, g);
|
||||
if (err == CRYPT_BUFFER_OVERFLOW) {
|
||||
goto retry;
|
||||
} else if (err == CRYPT_OK) {
|
||||
err = pem_read((void**)&asn1_cert, &w, &hdr, g);
|
||||
if (err == CRYPT_OK) {
|
||||
break;
|
||||
} else if (err != CRYPT_UNKNOWN_PEM) {
|
||||
goto cleanup;
|
||||
|
|
@ -204,7 +199,7 @@ int pem_decode_pkcs_filehandle(FILE *f, ltc_pka_key *k, const password_ctx *pw_c
|
|||
LTC_ARGCHK(f != NULL);
|
||||
LTC_ARGCHK(k != NULL);
|
||||
{
|
||||
struct get_char g = { .get = pem_get_char_from_file, .data.f = f };
|
||||
struct get_char g = pem_get_char_init_filehandle(f);
|
||||
return s_decode(&g, k, pw_ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -216,7 +211,7 @@ int pem_decode_pkcs(const void *buf, unsigned long len, ltc_pka_key *k, const pa
|
|||
LTC_ARGCHK(len != 0);
|
||||
LTC_ARGCHK(k != NULL);
|
||||
{
|
||||
struct get_char g = { .get = pem_get_char_from_buf, SET_BUFP(.data.buf, buf, len) };
|
||||
struct get_char g = pem_get_char_init(buf, len);
|
||||
return s_decode(&g, k, pw_ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,98 @@ extern const struct str pem_dek_info_start;
|
|||
extern const struct blockcipher_info pem_dek_infos[];
|
||||
extern const unsigned long pem_dek_infos_num;
|
||||
|
||||
#ifndef LTC_NO_FILE
|
||||
int pem_get_char_from_file(struct get_char *g)
|
||||
static LTC_INLINE unsigned long s_bufp_alloc_len(struct bufp *buf)
|
||||
{
|
||||
return getc(g->data.f);
|
||||
if (buf->start == NULL || buf->end == NULL)
|
||||
return 0;
|
||||
return buf->end - buf->start - 1;
|
||||
}
|
||||
|
||||
static LTC_INLINE unsigned long s_bufp_used_len(struct bufp *buf)
|
||||
{
|
||||
if (buf->start == NULL || buf->end == NULL)
|
||||
return 0;
|
||||
return buf->work - buf->start;
|
||||
}
|
||||
|
||||
static LTC_INLINE int s_bufp_grow(struct bufp *buf)
|
||||
{
|
||||
int err = CRYPT_OK;
|
||||
void *ret;
|
||||
unsigned long alloc_len = s_bufp_alloc_len(buf), realloc_len;
|
||||
unsigned long work_offset = s_bufp_used_len(buf);
|
||||
if (alloc_len == 0)
|
||||
realloc_len = LTC_PEM_READ_BUFSIZE;
|
||||
else
|
||||
realloc_len = alloc_len * 2;
|
||||
if (realloc_len < alloc_len)
|
||||
return CRYPT_OVERFLOW;
|
||||
ret = XREALLOC(buf->start, realloc_len);
|
||||
if (ret == NULL) {
|
||||
err = CRYPT_MEM;
|
||||
} else {
|
||||
UPDATE_BUFP((*buf), ret, work_offset, realloc_len);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static LTC_INLINE int s_bufp_fits(struct bufp *buf, unsigned long to_write)
|
||||
{
|
||||
char *d = buf->work;
|
||||
char *e = buf->end;
|
||||
char *w = d + to_write;
|
||||
if (d == NULL || w < d || w > e)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static LTC_INLINE int s_bufp_add(struct bufp *buf, const void *src, unsigned long len)
|
||||
{
|
||||
int err;
|
||||
if (!s_bufp_fits(buf, len)) {
|
||||
if ((err = s_bufp_grow(buf)) != CRYPT_OK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
XMEMCPY(buf->work, src, len);
|
||||
buf->work += len;
|
||||
return CRYPT_OK;
|
||||
}
|
||||
|
||||
#ifndef LTC_NO_FILE
|
||||
static int s_pem_get_char_from_file(struct get_char *g)
|
||||
{
|
||||
return getc(g->data.f.f);
|
||||
}
|
||||
|
||||
static int s_pem_get_char_reset_file(struct get_char *g)
|
||||
{
|
||||
if (g->data.f.original_pos == -1)
|
||||
return -1;
|
||||
return fseek(g->data.f.f, g->data.f.original_pos, SEEK_SET);
|
||||
}
|
||||
|
||||
static unsigned long s_pem_get_char_len_file(struct get_char *g)
|
||||
{
|
||||
FILE *f = g->data.f.f;
|
||||
long len, cur_pos = ftell(f);
|
||||
if (cur_pos != -1) {
|
||||
fseek(f, 0, SEEK_END);
|
||||
len = ftell(f);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
return len - cur_pos;
|
||||
}
|
||||
return LTC_PEM_READ_BUFSIZE * 2;
|
||||
}
|
||||
|
||||
const struct get_char_api get_char_filehandle_api = {
|
||||
.get = s_pem_get_char_from_file,
|
||||
.reset = s_pem_get_char_reset_file,
|
||||
.len = s_pem_get_char_len_file,
|
||||
};
|
||||
#endif /* LTC_NO_FILE */
|
||||
|
||||
int pem_get_char_from_buf(struct get_char *g)
|
||||
static int s_pem_get_char_from_buf(struct get_char *g)
|
||||
{
|
||||
int ret;
|
||||
if (g->data.buf.work == g->data.buf.end) {
|
||||
|
|
@ -35,6 +119,25 @@ int pem_get_char_from_buf(struct get_char *g)
|
|||
return ret;
|
||||
}
|
||||
|
||||
static int s_pem_get_char_reset(struct get_char *g)
|
||||
{
|
||||
g->data.buf.work = g->data.buf.start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static unsigned long s_pem_get_char_len(struct get_char *g)
|
||||
{
|
||||
return s_bufp_alloc_len(&g->data.buf);
|
||||
}
|
||||
|
||||
const struct get_char_api get_char_buffer_api = {
|
||||
.get = s_pem_get_char_from_buf,
|
||||
.reset = s_pem_get_char_reset,
|
||||
.len = s_pem_get_char_len,
|
||||
};
|
||||
|
||||
static void s_unget_line(char *buf, unsigned long buflen, struct get_char *g)
|
||||
{
|
||||
if (buflen > sizeof(g->unget_buf_))
|
||||
|
|
@ -81,7 +184,7 @@ static char* s_get_line_i(char *buf, unsigned long *buflen, struct get_char *g,
|
|||
while(blen < *buflen || search_for_start) {
|
||||
wr = blen < *buflen ? blen : *buflen - 1;
|
||||
c_ = g->prev_get;
|
||||
g->prev_get = g->get(g);
|
||||
g->prev_get = g->api.get(g);
|
||||
if (g->prev_get == '\n') {
|
||||
buf[wr] = '\0';
|
||||
if (c_ == '\r') {
|
||||
|
|
@ -103,26 +206,16 @@ static char* s_get_line_i(char *buf, unsigned long *buflen, struct get_char *g,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
LTC_INLINE static char* s_get_first_line(char *buf, unsigned long *buflen, struct get_char *g)
|
||||
static LTC_INLINE char* s_get_first_line(char *buf, unsigned long *buflen, struct get_char *g)
|
||||
{
|
||||
return s_get_line_i(buf, buflen, g, 1);
|
||||
}
|
||||
|
||||
LTC_INLINE static char* s_get_line(char *buf, unsigned long *buflen, struct get_char *g)
|
||||
static LTC_INLINE char* s_get_line(char *buf, unsigned long *buflen, struct get_char *g)
|
||||
{
|
||||
return s_get_line_i(buf, buflen, g, 0);
|
||||
}
|
||||
|
||||
static LTC_INLINE int s_fits_buf(void *dest, unsigned long to_write, void *end)
|
||||
{
|
||||
unsigned char *d = dest;
|
||||
unsigned char *e = end;
|
||||
unsigned char *w = d + to_write;
|
||||
if (w < d || w > e)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int s_pem_decode_headers(struct pem_headers *hdr, struct get_char *g)
|
||||
{
|
||||
char buf[LTC_PEM_DECODE_BUFSZ], *alg_start;
|
||||
|
|
@ -190,21 +283,19 @@ static int s_pem_decode_headers(struct pem_headers *hdr, struct get_char *g)
|
|||
return CRYPT_OK;
|
||||
}
|
||||
|
||||
int pem_read(void *asn1_cert, unsigned long *asn1_len, struct pem_headers *hdr, struct get_char *g)
|
||||
int pem_read(void **dest, unsigned long *len, struct pem_headers *hdr, struct get_char *g)
|
||||
{
|
||||
char buf[LTC_PEM_DECODE_BUFSZ];
|
||||
char *wpem = asn1_cert;
|
||||
char *end = wpem + *asn1_len;
|
||||
char line[LTC_PEM_DECODE_BUFSZ];
|
||||
struct bufp b_ = {0}, *b = &b_;
|
||||
const char pem_start[] = "----";
|
||||
unsigned long slen, linelen;
|
||||
int err, hdr_ok = 0;
|
||||
int would_overflow = 0;
|
||||
unsigned char empty_lines = 0;
|
||||
|
||||
g->prev_get = 0;
|
||||
do {
|
||||
linelen = sizeof(buf);
|
||||
if (s_get_first_line(buf, &linelen, g) == NULL) {
|
||||
linelen = sizeof(line);
|
||||
if (s_get_first_line(line, &linelen, g) == NULL) {
|
||||
if (g->prev_get == -1)
|
||||
return CRYPT_NOP;
|
||||
else
|
||||
|
|
@ -212,9 +303,9 @@ int pem_read(void *asn1_cert, unsigned long *asn1_len, struct pem_headers *hdr,
|
|||
}
|
||||
if (linelen < sizeof(pem_start) - 1)
|
||||
continue;
|
||||
} while(XMEMCMP(buf, pem_start, sizeof(pem_start) - 1) != 0);
|
||||
if (hdr->id->start.len != linelen || XMEMCMP(buf, hdr->id->start.p, hdr->id->start.len)) {
|
||||
s_unget_line(buf, linelen, g);
|
||||
} while(XMEMCMP(line, pem_start, sizeof(pem_start) - 1) != 0);
|
||||
if (hdr->id->start.len != linelen || XMEMCMP(line, hdr->id->start.p, hdr->id->start.len)) {
|
||||
s_unget_line(line, linelen, g);
|
||||
return CRYPT_UNKNOWN_PEM;
|
||||
}
|
||||
|
||||
|
|
@ -223,9 +314,9 @@ int pem_read(void *asn1_cert, unsigned long *asn1_len, struct pem_headers *hdr,
|
|||
return err;
|
||||
|
||||
/* Read the base64 encoded part of the PEM */
|
||||
slen = sizeof(buf);
|
||||
while (s_get_line(buf, &slen, g)) {
|
||||
if (slen == hdr->id->end.len && !XMEMCMP(buf, hdr->id->end.p, slen)) {
|
||||
slen = sizeof(line);
|
||||
while (s_get_line(line, &slen, g)) {
|
||||
if (slen == hdr->id->end.len && !XMEMCMP(line, hdr->id->end.p, slen)) {
|
||||
hdr_ok = 1;
|
||||
break;
|
||||
}
|
||||
|
|
@ -234,34 +325,25 @@ int pem_read(void *asn1_cert, unsigned long *asn1_len, struct pem_headers *hdr,
|
|||
break;
|
||||
empty_lines++;
|
||||
}
|
||||
if (!would_overflow && s_fits_buf(wpem, slen, end)) {
|
||||
XMEMCPY(wpem, buf, slen);
|
||||
} else {
|
||||
would_overflow = 1;
|
||||
if ((err = s_bufp_add(b, line, slen)) != CRYPT_OK) {
|
||||
goto error_out;
|
||||
}
|
||||
wpem += slen;
|
||||
slen = sizeof(buf);
|
||||
slen = sizeof(line);
|
||||
}
|
||||
if (!hdr_ok)
|
||||
return CRYPT_INVALID_PACKET;
|
||||
|
||||
if (would_overflow || !s_fits_buf(wpem, 1, end)) {
|
||||
/* NUL termination */
|
||||
wpem++;
|
||||
/* prevent a wrap-around */
|
||||
if (wpem < (char*)asn1_cert)
|
||||
return CRYPT_OVERFLOW;
|
||||
*asn1_len = wpem - (char*)asn1_cert;
|
||||
return CRYPT_BUFFER_OVERFLOW;
|
||||
if (!hdr_ok) {
|
||||
err = CRYPT_INVALID_PACKET;
|
||||
} else {
|
||||
slen = s_bufp_alloc_len(b);
|
||||
err = base64_strict_decode(b->start, s_bufp_used_len(b), (void*)b->start, &slen);
|
||||
}
|
||||
|
||||
*asn1_len = wpem - (char*)asn1_cert;
|
||||
*wpem++ = '\0';
|
||||
|
||||
if ((err = base64_strict_decode(asn1_cert, *asn1_len, asn1_cert, asn1_len)) != CRYPT_OK) {
|
||||
return err;
|
||||
if (err == CRYPT_OK) {
|
||||
*dest = b->start;
|
||||
*len = slen;
|
||||
} else {
|
||||
error_out:
|
||||
XFREE(b->start);
|
||||
}
|
||||
return CRYPT_OK;
|
||||
return err;
|
||||
}
|
||||
|
||||
#endif /* LTC_PEM */
|
||||
|
|
|
|||
|
|
@ -712,20 +712,15 @@ static const unsigned long pem_openssh_num = LTC_ARRAY_SIZE(pem_openssh);
|
|||
static int s_decode_openssh(struct get_char *g, ltc_pka_key *k, const password_ctx *pw_ctx)
|
||||
{
|
||||
unsigned char *pem = NULL, *p, *privkey = NULL, *tag;
|
||||
unsigned long n, w, l, privkey_len, taglen;
|
||||
unsigned long n, w = 0, l, privkey_len, taglen;
|
||||
int err;
|
||||
struct pem_headers hdr;
|
||||
struct kdf_options opts = { 0 };
|
||||
XMEMSET(k, 0, sizeof(*k));
|
||||
w = LTC_PEM_READ_BUFSIZE * 2;
|
||||
retry:
|
||||
pem = XREALLOC(pem, w);
|
||||
for (n = 0; n < pem_openssh_num; ++n) {
|
||||
hdr.id = &pem_openssh[n];
|
||||
err = pem_read(pem, &w, &hdr, g);
|
||||
if (err == CRYPT_BUFFER_OVERFLOW) {
|
||||
goto retry;
|
||||
} else if (err == CRYPT_OK) {
|
||||
err = pem_read((void**)&pem, &w, &hdr, g);
|
||||
if (err == CRYPT_OK) {
|
||||
break;
|
||||
} else if (err != CRYPT_UNKNOWN_PEM) {
|
||||
goto cleanup;
|
||||
|
|
@ -791,7 +786,9 @@ cleanup:
|
|||
zeromem(privkey, privkey_len);
|
||||
XFREE(privkey);
|
||||
}
|
||||
XFREE(pem);
|
||||
if (pem) {
|
||||
XFREE(pem);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
|
|
@ -801,7 +798,7 @@ int pem_decode_openssh_filehandle(FILE *f, ltc_pka_key *k, const password_ctx *p
|
|||
LTC_ARGCHK(f != NULL);
|
||||
LTC_ARGCHK(k != NULL);
|
||||
{
|
||||
struct get_char g = { .get = pem_get_char_from_file, .data.f = f };
|
||||
struct get_char g = pem_get_char_init_filehandle(f);
|
||||
return s_decode_openssh(&g, k, pw_ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -841,7 +838,7 @@ int pem_decode_openssh(const void *buf, unsigned long len, ltc_pka_key *k, const
|
|||
LTC_ARGCHK(len != 0);
|
||||
LTC_ARGCHK(k != NULL);
|
||||
{
|
||||
struct get_char g = { .get = pem_get_char_from_buf, SET_BUFP(.data.buf, buf, len) };
|
||||
struct get_char g = pem_get_char_init(buf, len);
|
||||
return s_decode_openssh(&g, k, pw_ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,35 +592,24 @@ static int s_x509_import_pem(struct get_char *g, unsigned long *pem_len, const l
|
|||
{
|
||||
int err;
|
||||
struct pem_headers hdr = { .id = &pem_std_headers[0] };
|
||||
unsigned long alloc_len = *pem_len, len;
|
||||
unsigned long len = 0;
|
||||
unsigned char *asn1_cert = NULL;
|
||||
len = alloc_len;
|
||||
retry:
|
||||
asn1_cert = XREALLOC(asn1_cert, alloc_len);
|
||||
if (asn1_cert == NULL) {
|
||||
err = CRYPT_MEM;
|
||||
goto err_out;
|
||||
}
|
||||
if ((err = pem_read(asn1_cert, &len, &hdr, g)) != CRYPT_OK) {
|
||||
if (err == CRYPT_BUFFER_OVERFLOW && alloc_len == *pem_len) {
|
||||
alloc_len = len;
|
||||
goto retry;
|
||||
}
|
||||
goto err_out;
|
||||
if ((err = pem_read((void**)&asn1_cert, &len, &hdr, g)) != CRYPT_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
err = x509_import(asn1_cert, len, out);
|
||||
|
||||
*pem_len = len;
|
||||
|
||||
err_out:
|
||||
XFREE(asn1_cert);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int x509_import_pem(const char *pem, unsigned long *pem_len, const ltc_x509_certificate **out)
|
||||
{
|
||||
struct get_char g = { .get = pem_get_char_from_buf, SET_BUFP(.data.buf, pem, *pem_len) };
|
||||
struct get_char g = pem_get_char_init(pem, *pem_len);
|
||||
return s_x509_import_pem(&g, pem_len, out);
|
||||
}
|
||||
|
||||
|
|
@ -628,17 +617,7 @@ int x509_import_pem(const char *pem, unsigned long *pem_len, const ltc_x509_cert
|
|||
int x509_import_pem_filehandle(FILE *f, const ltc_x509_certificate **out)
|
||||
{
|
||||
unsigned long pem_len;
|
||||
long cur_pos;
|
||||
struct get_char g = { .get = pem_get_char_from_file, .data.f = f };
|
||||
cur_pos = ftell(f);
|
||||
if (cur_pos != -1) {
|
||||
fseek(f, 0, SEEK_END);
|
||||
pem_len = ftell(f);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
pem_len -= cur_pos;
|
||||
} else {
|
||||
pem_len = LTC_PEM_READ_BUFSIZE;
|
||||
}
|
||||
struct get_char g = pem_get_char_init_filehandle(f);
|
||||
return s_x509_import_pem(&g, &pem_len, out);
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue