sync code with last fixes and improvements from OpenBSD
This commit is contained in:
parent
4b78db449c
commit
bf0676207f
2406 changed files with 6353 additions and 434004 deletions
|
@ -1,4 +1,4 @@
|
|||
/* $OpenBSD: bn_arch.h,v 1.7 2023/04/17 12:51:09 jsing Exp $ */
|
||||
/* $OpenBSD: bn_arch.h,v 1.9 2023/05/28 17:42:30 jsing Exp $ */
|
||||
/*
|
||||
* Copyright (c) 2023 Joel Sing <jsing@openbsd.org>
|
||||
*
|
||||
|
@ -42,6 +42,27 @@ bn_addw(BN_ULONG a, BN_ULONG b, BN_ULONG *out_r1, BN_ULONG *out_r0)
|
|||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_ADDW_ADDW
|
||||
|
||||
static inline void
|
||||
bn_addw_addw(BN_ULONG a, BN_ULONG b, BN_ULONG c, BN_ULONG *out_r1,
|
||||
BN_ULONG *out_r0)
|
||||
{
|
||||
BN_ULONG carry, r0;
|
||||
|
||||
__asm__ (
|
||||
"adds %[r0], %[a], %[b] \n"
|
||||
"cset %[carry], cs \n"
|
||||
"adds %[r0], %[r0], %[c] \n"
|
||||
"cinc %[carry], %[carry], cs \n"
|
||||
: [carry]"=&r"(carry), [r0]"=&r"(r0)
|
||||
: [a]"r"(a), [b]"r"(b), [c]"r"(c)
|
||||
: "cc");
|
||||
|
||||
*out_r1 = carry;
|
||||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_MULW
|
||||
|
||||
static inline void
|
||||
|
@ -60,6 +81,73 @@ bn_mulw(BN_ULONG a, BN_ULONG b, BN_ULONG *out_r1, BN_ULONG *out_r0)
|
|||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_MULW_ADDW
|
||||
|
||||
static inline void
|
||||
bn_mulw_addw(BN_ULONG a, BN_ULONG b, BN_ULONG c, BN_ULONG *out_r1,
|
||||
BN_ULONG *out_r0)
|
||||
{
|
||||
BN_ULONG r1, r0;
|
||||
|
||||
__asm__ (
|
||||
"umulh %[r1], %[a], %[b] \n"
|
||||
"mul %[r0], %[a], %[b] \n"
|
||||
"adds %[r0], %[r0], %[c] \n"
|
||||
"adc %[r1], %[r1], xzr \n"
|
||||
: [r1]"=&r"(r1), [r0]"=&r"(r0)
|
||||
: [a]"r"(a), [b]"r"(b), [c]"r"(c)
|
||||
: "cc");
|
||||
|
||||
*out_r1 = r1;
|
||||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_MULW_ADDW_ADDW
|
||||
|
||||
static inline void
|
||||
bn_mulw_addw_addw(BN_ULONG a, BN_ULONG b, BN_ULONG c, BN_ULONG d,
|
||||
BN_ULONG *out_r1, BN_ULONG *out_r0)
|
||||
{
|
||||
BN_ULONG r1, r0;
|
||||
|
||||
__asm__ (
|
||||
"umulh %[r1], %[a], %[b] \n"
|
||||
"mul %[r0], %[a], %[b] \n"
|
||||
"adds %[r0], %[r0], %[c] \n"
|
||||
"adc %[r1], %[r1], xzr \n"
|
||||
"adds %[r0], %[r0], %[d] \n"
|
||||
"adc %[r1], %[r1], xzr \n"
|
||||
: [r1]"=&r"(r1), [r0]"=&r"(r0)
|
||||
: [a]"r"(a), [b]"r"(b), [c]"r"(c), [d]"r"(d)
|
||||
: "cc");
|
||||
|
||||
*out_r1 = r1;
|
||||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_MULW_ADDTW
|
||||
|
||||
static inline void
|
||||
bn_mulw_addtw(BN_ULONG a, BN_ULONG b, BN_ULONG c2, BN_ULONG c1, BN_ULONG c0,
|
||||
BN_ULONG *out_r2, BN_ULONG *out_r1, BN_ULONG *out_r0)
|
||||
{
|
||||
BN_ULONG r2, r1, r0;
|
||||
|
||||
__asm__ (
|
||||
"umulh %[r1], %[a], %[b] \n"
|
||||
"mul %[r0], %[a], %[b] \n"
|
||||
"adds %[r0], %[r0], %[c0] \n"
|
||||
"adcs %[r1], %[r1], %[c1] \n"
|
||||
"adc %[r2], xzr, %[c2] \n"
|
||||
: [r2]"=&r"(r2), [r1]"=&r"(r1), [r0]"=&r"(r0)
|
||||
: [a]"r"(a), [b]"r"(b), [c2]"r"(c2), [c1]"r"(c1), [c0]"r"(c0)
|
||||
: "cc");
|
||||
|
||||
*out_r2 = r2;
|
||||
*out_r1 = r1;
|
||||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_SUBW
|
||||
|
||||
static inline void
|
||||
|
@ -78,6 +166,27 @@ bn_subw(BN_ULONG a, BN_ULONG b, BN_ULONG *out_borrow, BN_ULONG *out_r0)
|
|||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#define HAVE_BN_SUBW_SUBW
|
||||
|
||||
static inline void
|
||||
bn_subw_subw(BN_ULONG a, BN_ULONG b, BN_ULONG c, BN_ULONG *out_borrow,
|
||||
BN_ULONG *out_r0)
|
||||
{
|
||||
BN_ULONG borrow, r0;
|
||||
|
||||
__asm__ (
|
||||
"subs %[r0], %[a], %[b] \n"
|
||||
"cset %[borrow], cc \n"
|
||||
"subs %[r0], %[r0], %[c] \n"
|
||||
"cinc %[borrow], %[borrow], cc \n"
|
||||
: [borrow]"=&r"(borrow), [r0]"=&r"(r0)
|
||||
: [a]"r"(a), [b]"r"(b), [c]"r"(c)
|
||||
: "cc");
|
||||
|
||||
*out_borrow = borrow;
|
||||
*out_r0 = r0;
|
||||
}
|
||||
|
||||
#endif /* __GNUC__ */
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* $OpenBSD: bn_convert.c,v 1.8 2023/05/09 05:15:55 jsing Exp $ */
|
||||
/* $OpenBSD: bn_convert.c,v 1.9 2023/05/28 10:34:17 jsing Exp $ */
|
||||
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
* All rights reserved.
|
||||
*
|
||||
|
@ -70,6 +70,9 @@
|
|||
#include "bn_local.h"
|
||||
#include "bytestring.h"
|
||||
|
||||
static int bn_dec2bn_cbs(BIGNUM **bnp, CBS *cbs);
|
||||
static int bn_hex2bn_cbs(BIGNUM **bnp, CBS *cbs);
|
||||
|
||||
static const char hex_digits[] = "0123456789ABCDEF";
|
||||
|
||||
typedef enum {
|
||||
|
@ -253,21 +256,53 @@ BN_lebin2bn(const unsigned char *s, int len, BIGNUM *ret)
|
|||
}
|
||||
|
||||
int
|
||||
BN_asc2bn(BIGNUM **bn, const char *a)
|
||||
BN_asc2bn(BIGNUM **bnp, const char *s)
|
||||
{
|
||||
const char *p = a;
|
||||
if (*p == '-')
|
||||
p++;
|
||||
CBS cbs, cbs_hex;
|
||||
size_t s_len;
|
||||
uint8_t v;
|
||||
int neg;
|
||||
|
||||
if (p[0] == '0' && (p[1] == 'X' || p[1] == 'x')) {
|
||||
if (!BN_hex2bn(bn, p + 2))
|
||||
return 0;
|
||||
} else {
|
||||
if (!BN_dec2bn(bn, p))
|
||||
if (bnp != NULL && *bnp != NULL)
|
||||
BN_zero(*bnp);
|
||||
|
||||
if (s == NULL)
|
||||
return 0;
|
||||
if ((s_len = strlen(s)) == 0)
|
||||
return 0;
|
||||
|
||||
CBS_init(&cbs, s, s_len);
|
||||
|
||||
/* Handle negative sign. */
|
||||
if (!CBS_peek_u8(&cbs, &v))
|
||||
return 0;
|
||||
if ((neg = (v == '-'))) {
|
||||
if (!CBS_skip(&cbs, 1))
|
||||
return 0;
|
||||
}
|
||||
if (*a == '-')
|
||||
BN_set_negative(*bn, 1);
|
||||
|
||||
/* Try parsing as hexidecimal with a 0x prefix. */
|
||||
CBS_dup(&cbs, &cbs_hex);
|
||||
if (!CBS_get_u8(&cbs_hex, &v))
|
||||
goto decimal;
|
||||
if (v != '0')
|
||||
goto decimal;
|
||||
if (!CBS_get_u8(&cbs_hex, &v))
|
||||
goto decimal;
|
||||
if (v != 'X' && v != 'x')
|
||||
goto decimal;
|
||||
if (!bn_hex2bn_cbs(bnp, &cbs_hex))
|
||||
return 0;
|
||||
|
||||
goto done;
|
||||
|
||||
decimal:
|
||||
if (!bn_dec2bn_cbs(bnp, &cbs))
|
||||
return 0;
|
||||
|
||||
done:
|
||||
BN_set_negative(*bnp, neg);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -349,73 +384,108 @@ BN_bn2dec(const BIGNUM *bn)
|
|||
return s;
|
||||
}
|
||||
|
||||
int
|
||||
BN_dec2bn(BIGNUM **bn, const char *a)
|
||||
static int
|
||||
bn_dec2bn_cbs(BIGNUM **bnp, CBS *cbs)
|
||||
{
|
||||
BIGNUM *ret = NULL;
|
||||
BN_ULONG l = 0;
|
||||
int neg = 0, i, j;
|
||||
int num;
|
||||
CBS cbs_digits;
|
||||
BIGNUM *bn = NULL;
|
||||
int d, neg, num;
|
||||
size_t digits = 0;
|
||||
BN_ULONG w;
|
||||
uint8_t v;
|
||||
|
||||
if ((a == NULL) || (*a == '\0'))
|
||||
return (0);
|
||||
if (*a == '-') {
|
||||
neg = 1;
|
||||
a++;
|
||||
/* Handle negative sign. */
|
||||
if (!CBS_peek_u8(cbs, &v))
|
||||
goto err;
|
||||
if ((neg = (v == '-'))) {
|
||||
if (!CBS_skip(cbs, 1))
|
||||
goto err;
|
||||
}
|
||||
|
||||
for (i = 0; i <= (INT_MAX / 4) && isdigit((unsigned char)a[i]); i++)
|
||||
;
|
||||
if (i > INT_MAX / 4)
|
||||
return (0);
|
||||
|
||||
num = i + neg;
|
||||
if (bn == NULL)
|
||||
return (num);
|
||||
|
||||
/* a is the start of the digits, and it is 'i' long.
|
||||
* We chop it into BN_DEC_NUM digits at a time */
|
||||
if (*bn == NULL) {
|
||||
if ((ret = BN_new()) == NULL)
|
||||
return (0);
|
||||
} else {
|
||||
ret = *bn;
|
||||
BN_zero(ret);
|
||||
/* Scan to find last decimal digit. */
|
||||
CBS_dup(cbs, &cbs_digits);
|
||||
while (CBS_len(&cbs_digits) > 0) {
|
||||
if (!CBS_get_u8(&cbs_digits, &v))
|
||||
goto err;
|
||||
if (!isdigit(v))
|
||||
break;
|
||||
digits++;
|
||||
}
|
||||
|
||||
/* i is the number of digits, a bit of an over expand */
|
||||
if (!bn_expand(ret, i * 4))
|
||||
if (digits > INT_MAX / 4)
|
||||
goto err;
|
||||
|
||||
j = BN_DEC_NUM - (i % BN_DEC_NUM);
|
||||
if (j == BN_DEC_NUM)
|
||||
j = 0;
|
||||
l = 0;
|
||||
while (*a) {
|
||||
l *= 10;
|
||||
l += *a - '0';
|
||||
a++;
|
||||
if (++j == BN_DEC_NUM) {
|
||||
if (!BN_mul_word(ret, BN_DEC_CONV))
|
||||
num = digits + neg;
|
||||
|
||||
if (bnp == NULL)
|
||||
return num;
|
||||
|
||||
if ((bn = *bnp) == NULL)
|
||||
bn = BN_new();
|
||||
if (bn == NULL)
|
||||
goto err;
|
||||
if (!bn_expand(bn, digits * 4))
|
||||
goto err;
|
||||
|
||||
if ((d = digits % BN_DEC_NUM) == 0)
|
||||
d = BN_DEC_NUM;
|
||||
|
||||
w = 0;
|
||||
|
||||
/* Work forwards from most significant digit. */
|
||||
while (digits-- > 0) {
|
||||
if (!CBS_get_u8(cbs, &v))
|
||||
goto err;
|
||||
|
||||
if (v < '0' || v > '9')
|
||||
goto err;
|
||||
|
||||
v -= '0';
|
||||
w = w * 10 + v;
|
||||
d--;
|
||||
|
||||
if (d == 0) {
|
||||
if (!BN_mul_word(bn, BN_DEC_CONV))
|
||||
goto err;
|
||||
if (!BN_add_word(ret, l))
|
||||
if (!BN_add_word(bn, w))
|
||||
goto err;
|
||||
l = 0;
|
||||
j = 0;
|
||||
|
||||
d = BN_DEC_NUM;
|
||||
w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bn_correct_top(ret);
|
||||
bn_correct_top(bn);
|
||||
|
||||
BN_set_negative(ret, neg);
|
||||
BN_set_negative(bn, neg);
|
||||
|
||||
*bn = ret;
|
||||
return (num);
|
||||
*bnp = bn;
|
||||
|
||||
err:
|
||||
if (*bn == NULL)
|
||||
BN_free(ret);
|
||||
return (0);
|
||||
return num;
|
||||
|
||||
err:
|
||||
if (bnp != NULL && *bnp == NULL)
|
||||
BN_free(bn);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
BN_dec2bn(BIGNUM **bnp, const char *s)
|
||||
{
|
||||
size_t s_len;
|
||||
CBS cbs;
|
||||
|
||||
if (bnp != NULL && *bnp != NULL)
|
||||
BN_zero(*bnp);
|
||||
|
||||
if (s == NULL)
|
||||
return 0;
|
||||
if ((s_len = strlen(s)) == 0)
|
||||
return 0;
|
||||
|
||||
CBS_init(&cbs, s, s_len);
|
||||
|
||||
return bn_dec2bn_cbs(bnp, &cbs);
|
||||
}
|
||||
|
||||
char *
|
||||
|
@ -463,81 +533,112 @@ BN_bn2hex(const BIGNUM *bn)
|
|||
return s;
|
||||
}
|
||||
|
||||
int
|
||||
BN_hex2bn(BIGNUM **bn, const char *a)
|
||||
static int
|
||||
bn_hex2bn_cbs(BIGNUM **bnp, CBS *cbs)
|
||||
{
|
||||
BIGNUM *ret = NULL;
|
||||
BN_ULONG l = 0;
|
||||
int neg = 0, h, m, i,j, k, c;
|
||||
int num;
|
||||
CBS cbs_digits;
|
||||
BIGNUM *bn = NULL;
|
||||
int b, i, neg, num;
|
||||
size_t digits = 0;
|
||||
BN_ULONG w;
|
||||
uint8_t v;
|
||||
|
||||
if ((a == NULL) || (*a == '\0'))
|
||||
return (0);
|
||||
|
||||
if (*a == '-') {
|
||||
neg = 1;
|
||||
a++;
|
||||
/* Handle negative sign. */
|
||||
if (!CBS_peek_u8(cbs, &v))
|
||||
goto err;
|
||||
if ((neg = (v == '-'))) {
|
||||
if (!CBS_skip(cbs, 1))
|
||||
goto err;
|
||||
}
|
||||
|
||||
for (i = 0; i <= (INT_MAX / 4) && isxdigit((unsigned char)a[i]); i++)
|
||||
;
|
||||
if (i > INT_MAX / 4)
|
||||
return (0);
|
||||
|
||||
num = i + neg;
|
||||
if (bn == NULL)
|
||||
return (num);
|
||||
|
||||
/* a is the start of the hex digits, and it is 'i' long */
|
||||
if (*bn == NULL) {
|
||||
if ((ret = BN_new()) == NULL)
|
||||
return (0);
|
||||
} else {
|
||||
ret = *bn;
|
||||
BN_zero(ret);
|
||||
/* Scan to find last hexadecimal digit. */
|
||||
CBS_dup(cbs, &cbs_digits);
|
||||
while (CBS_len(&cbs_digits) > 0) {
|
||||
if (!CBS_get_u8(&cbs_digits, &v))
|
||||
goto err;
|
||||
if (!isxdigit(v))
|
||||
break;
|
||||
digits++;
|
||||
}
|
||||
|
||||
/* i is the number of hex digits */
|
||||
if (!bn_expand(ret, i * 4))
|
||||
if (digits > INT_MAX / 4)
|
||||
goto err;
|
||||
|
||||
j = i; /* least significant 'hex' */
|
||||
m = 0;
|
||||
h = 0;
|
||||
while (j > 0) {
|
||||
m = ((BN_BYTES * 2) <= j) ? (BN_BYTES * 2) : j;
|
||||
l = 0;
|
||||
for (;;) {
|
||||
c = a[j - m];
|
||||
if ((c >= '0') && (c <= '9'))
|
||||
k = c - '0';
|
||||
else if ((c >= 'a') && (c <= 'f'))
|
||||
k = c - 'a' + 10;
|
||||
else if ((c >= 'A') && (c <= 'F'))
|
||||
k = c - 'A' + 10;
|
||||
else
|
||||
k = 0; /* paranoia */
|
||||
l = (l << 4) | k;
|
||||
num = digits + neg;
|
||||
|
||||
if (--m <= 0) {
|
||||
ret->d[h++] = l;
|
||||
break;
|
||||
}
|
||||
if (bnp == NULL)
|
||||
return num;
|
||||
|
||||
if ((bn = *bnp) == NULL)
|
||||
bn = BN_new();
|
||||
if (bn == NULL)
|
||||
goto err;
|
||||
if (!bn_expand(bn, digits * 4))
|
||||
goto err;
|
||||
|
||||
if (!CBS_get_bytes(cbs, cbs, digits))
|
||||
goto err;
|
||||
|
||||
b = BN_BITS2;
|
||||
i = 0;
|
||||
w = 0;
|
||||
|
||||
/* Work backwards from least significant digit. */
|
||||
while (digits-- > 0) {
|
||||
if (!CBS_get_last_u8(cbs, &v))
|
||||
goto err;
|
||||
|
||||
if (v >= '0' && v <= '9')
|
||||
v -= '0';
|
||||
else if (v >= 'a' && v <= 'f')
|
||||
v -= 'a' - 10;
|
||||
else if (v >= 'A' && v <= 'F')
|
||||
v -= 'A' - 10;
|
||||
else
|
||||
goto err;
|
||||
|
||||
w |= (BN_ULONG)v << (BN_BITS2 - b);
|
||||
b -= 4;
|
||||
|
||||
if (b == 0 || digits == 0) {
|
||||
b = BN_BITS2;
|
||||
bn->d[i++] = w;
|
||||
w = 0;
|
||||
}
|
||||
j -= (BN_BYTES * 2);
|
||||
}
|
||||
ret->top = h;
|
||||
bn_correct_top(ret);
|
||||
|
||||
BN_set_negative(ret, neg);
|
||||
bn->top = i;
|
||||
bn_correct_top(bn);
|
||||
|
||||
*bn = ret;
|
||||
return (num);
|
||||
BN_set_negative(bn, neg);
|
||||
|
||||
err:
|
||||
if (*bn == NULL)
|
||||
BN_free(ret);
|
||||
return (0);
|
||||
*bnp = bn;
|
||||
|
||||
return num;
|
||||
|
||||
err:
|
||||
if (bnp != NULL && *bnp == NULL)
|
||||
BN_free(bn);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
BN_hex2bn(BIGNUM **bnp, const char *s)
|
||||
{
|
||||
size_t s_len;
|
||||
CBS cbs;
|
||||
|
||||
if (bnp != NULL && *bnp != NULL)
|
||||
BN_zero(*bnp);
|
||||
|
||||
if (s == NULL)
|
||||
return 0;
|
||||
if ((s_len = strlen(s)) == 0)
|
||||
return 0;
|
||||
|
||||
CBS_init(&cbs, s, s_len);
|
||||
|
||||
return bn_hex2bn_cbs(bnp, &cbs);
|
||||
}
|
||||
|
||||
int
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* $OpenBSD: bn_gcd.c,v 1.27 2023/04/09 18:38:59 tb Exp $ */
|
||||
/* $OpenBSD: bn_gcd.c,v 1.28 2023/06/02 17:15:30 tb Exp $ */
|
||||
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
* All rights reserved.
|
||||
*
|
||||
|
@ -497,19 +497,16 @@ BN_mod_inverse_no_branch(BIGNUM *in, const BIGNUM *a, const BIGNUM *n,
|
|||
}
|
||||
/* Now Y*a == A (mod |n|). */
|
||||
|
||||
if (BN_is_one(A)) {
|
||||
/* Y*a == 1 (mod |n|) */
|
||||
if (!Y->neg && BN_ucmp(Y, n) < 0) {
|
||||
if (!bn_copy(R, Y))
|
||||
goto err;
|
||||
} else {
|
||||
if (!BN_nnmod(R, Y, n, ctx))
|
||||
goto err;
|
||||
}
|
||||
} else {
|
||||
if (!BN_is_one(A)) {
|
||||
BNerror(BN_R_NO_INVERSE);
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (!BN_nnmod(Y, Y, n, ctx))
|
||||
goto err;
|
||||
if (!bn_copy(R, Y))
|
||||
goto err;
|
||||
|
||||
ret = R;
|
||||
|
||||
err:
|
||||
|
@ -785,19 +782,16 @@ BN_mod_inverse_internal(BIGNUM *in, const BIGNUM *a, const BIGNUM *n, BN_CTX *ct
|
|||
}
|
||||
/* Now Y*a == A (mod |n|). */
|
||||
|
||||
if (BN_is_one(A)) {
|
||||
/* Y*a == 1 (mod |n|) */
|
||||
if (!Y->neg && BN_ucmp(Y, n) < 0) {
|
||||
if (!bn_copy(R, Y))
|
||||
goto err;
|
||||
} else {
|
||||
if (!BN_nnmod(R, Y,n, ctx))
|
||||
goto err;
|
||||
}
|
||||
} else {
|
||||
if (!BN_is_one(A)) {
|
||||
BNerror(BN_R_NO_INVERSE);
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (!BN_nnmod(Y, Y, n, ctx))
|
||||
goto err;
|
||||
if (!bn_copy(R, Y))
|
||||
goto err;
|
||||
|
||||
ret = R;
|
||||
|
||||
err:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* $OpenBSD: bn_isqrt.c,v 1.8 2023/05/17 07:42:38 tb Exp $ */
|
||||
/* $OpenBSD: bn_isqrt.c,v 1.10 2023/06/04 17:28:35 tb Exp $ */
|
||||
/*
|
||||
* Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue