mirror of
git://git.gnupg.org/gnupg.git
synced 2025-07-03 22:56:33 +02:00
Paul's LFS pacthes and started with pipemode
This commit is contained in:
parent
91451a22c6
commit
99e70f7ac7
26 changed files with 1283 additions and 769 deletions
|
@ -1,5 +1,36 @@
|
|||
2000-11-11 Paul Eggert <eggert@twinsun.com>
|
||||
|
||||
Clean up the places in the code that incorrectly use "long" or
|
||||
"unsigned long" for file offsets. The correct type to use is
|
||||
"off_t". The difference is important on large-file hosts,
|
||||
where "off_t" is longer than "long".
|
||||
|
||||
* keydb.h (struct keyblock_pos_struct.offset):
|
||||
Use off_t, not ulong, for file offsets.
|
||||
* packet.h (dbg_search_packet, dbg_copy_some_packets,
|
||||
search_packet, copy_some_packets): Likewise.
|
||||
* parse-packet.c (parse, dbg_search_packet, search_packet,
|
||||
dbg_copy_some_packets, copy_some_packets): Likewise.
|
||||
* ringedit.c (keyring_search): Likewise.
|
||||
|
||||
* parse-packet.c (parse): Do not use %lu to report file
|
||||
offsets in error diagnostics; it's not portable.
|
||||
* ringedit.c (keyring_search): Likewise.
|
||||
|
||||
2000-11-09 Werner Koch <wk@gnupg.org>
|
||||
|
||||
* g10.c (main): New option --enable-special-filenames.
|
||||
|
||||
2000-11-07 Werner Koch <wk@gnupg.org>
|
||||
|
||||
* g10.c (main): New command --pipemode.
|
||||
* pipemode.c: New.
|
||||
|
||||
2000-10-23 Werner Koch <wk@gnupg.org>
|
||||
|
||||
* armor.c (armor_filter): Changed output of hdrlines, so that a CR
|
||||
is emitted for DOS systems.
|
||||
|
||||
* keygen.c (read_parameter_file): Add a cast for isspace().
|
||||
|
||||
* status.c (myread): Use SIGINT instead of SIGHUP for DOS.
|
||||
|
|
|
@ -65,6 +65,7 @@ gpg_SOURCES = g10.c \
|
|||
tdbio.h \
|
||||
delkey.c \
|
||||
keygen.c \
|
||||
pipemode.c \
|
||||
helptext.c
|
||||
|
||||
gpgv_SOURCES = gpgv.c \
|
||||
|
|
11
g10/armor.c
11
g10/armor.c
|
@ -928,8 +928,15 @@ armor_filter( void *opaque, int control,
|
|||
iobuf_writestr(a, LF );
|
||||
}
|
||||
|
||||
if( afx->hdrlines )
|
||||
iobuf_writestr(a, afx->hdrlines);
|
||||
if ( afx->hdrlines ) {
|
||||
for ( s = afx->hdrlines; *s; s++ ) {
|
||||
#ifdef HAVE_DOSISH_SYSTEM
|
||||
if ( *s == '\n' )
|
||||
iobuf_put( a, '\r');
|
||||
#endif
|
||||
iobuf_put(a, *s );
|
||||
}
|
||||
}
|
||||
iobuf_writestr(a, LF );
|
||||
afx->status++;
|
||||
afx->idx = 0;
|
||||
|
|
15
g10/g10.c
15
g10/g10.c
|
@ -107,6 +107,7 @@ enum cmd_and_opt_values { aNull = 0,
|
|||
aDeArmor,
|
||||
aEnArmor,
|
||||
aGenRandom,
|
||||
aPipeMode,
|
||||
|
||||
oTextmode,
|
||||
oFingerprint,
|
||||
|
@ -183,6 +184,7 @@ enum cmd_and_opt_values { aNull = 0,
|
|||
oDisablePubkeyAlgo,
|
||||
oAllowNonSelfsignedUID,
|
||||
oAllowFreeformUID,
|
||||
oEnableSpecialFilenames,
|
||||
oNoLiteral,
|
||||
oSetFilesize,
|
||||
oHonorHttpProxy,
|
||||
|
@ -328,6 +330,7 @@ static ARGPARSE_OPTS opts[] = {
|
|||
{ aPrintMDs, "print-mds" , 256, "@"}, /* old */
|
||||
{ aListTrustDB, "list-trustdb",0 , "@"},
|
||||
{ aListTrustPath, "list-trust-path",0, "@"},
|
||||
{ aPipeMode, "pipemode", 0, "@" },
|
||||
{ oKOption, NULL, 0, "@"},
|
||||
{ oPasswdFD, "passphrase-fd",1, "@" },
|
||||
{ oCommandFD, "command-fd",1, "@" },
|
||||
|
@ -386,6 +389,7 @@ static ARGPARSE_OPTS opts[] = {
|
|||
{ oNoAutoKeyRetrieve, "no-auto-key-retrieve", 0, "@" },
|
||||
{ oMergeOnly, "merge-only", 0, "@" },
|
||||
{ oTryAllSecrets, "try-all-secrets", 0, "@" },
|
||||
{ oEnableSpecialFilenames, "enable-special-filenames", 0, "@" },
|
||||
{ oEmu3DESS2KBug, "emulate-3des-s2k-bug", 0, "@"},
|
||||
{ oEmuMDEncodeBug, "emulate-md-encode-bug", 0, "@"},
|
||||
{0} };
|
||||
|
@ -766,6 +770,7 @@ main( int argc, char **argv )
|
|||
case aEnArmor: set_cmd( &cmd, aEnArmor); break;
|
||||
case aExportOwnerTrust: set_cmd( &cmd, aExportOwnerTrust); break;
|
||||
case aImportOwnerTrust: set_cmd( &cmd, aImportOwnerTrust); break;
|
||||
case aPipeMode: set_cmd( &cmd, aPipeMode); break;
|
||||
|
||||
case oArmor: opt.armor = 1; opt.no_armor=0; break;
|
||||
case oOutput: opt.outfile = pargs.r.ret_str; break;
|
||||
|
@ -940,7 +945,9 @@ main( int argc, char **argv )
|
|||
case oMergeOnly: opt.merge_only = 1; break;
|
||||
case oTryAllSecrets: opt.try_all_secrets = 1; break;
|
||||
case oTrustedKey: register_trusted_key( pargs.r.ret_str ); break;
|
||||
|
||||
case oEnableSpecialFilenames:
|
||||
iobuf_enable_special_filenames (1);
|
||||
break;
|
||||
default : pargs.err = configfp? 1:2; break;
|
||||
}
|
||||
}
|
||||
|
@ -1515,6 +1522,12 @@ main( int argc, char **argv )
|
|||
wrong_args("--import-ownertrust [file]");
|
||||
import_ownertrust( argc? *argv:NULL );
|
||||
break;
|
||||
|
||||
case aPipeMode:
|
||||
if ( argc )
|
||||
wrong_args ("--pipemode");
|
||||
run_in_pipemode ();
|
||||
break;
|
||||
|
||||
case aListPackets:
|
||||
opt.list_packets=1;
|
||||
|
|
|
@ -69,7 +69,7 @@ enum resource_type {
|
|||
struct keyblock_pos_struct {
|
||||
int resno; /* resource number */
|
||||
enum resource_type rt;
|
||||
ulong offset; /* position information */
|
||||
off_t offset; /* position information */
|
||||
unsigned count; /* length of the keyblock in packets */
|
||||
IOBUF fp; /* used by enum_keyblocks */
|
||||
int secret; /* working on a secret keyring */
|
||||
|
|
|
@ -154,6 +154,9 @@ int decrypt_message( const char *filename );
|
|||
int hash_datafiles( MD_HANDLE md, MD_HANDLE md2,
|
||||
STRLIST files, const char *sigfilename, int textmode );
|
||||
|
||||
/*-- pipemode.c --*/
|
||||
void run_in_pipemode (void);
|
||||
|
||||
/*-- signal.c --*/
|
||||
void init_signals(void);
|
||||
void pause_on_sigusr( int which );
|
||||
|
|
34
g10/packet.h
34
g10/packet.h
|
@ -270,21 +270,31 @@ int list_packets( IOBUF a );
|
|||
int set_packet_list_mode( int mode );
|
||||
|
||||
#if DEBUG_PARSE_PACKET
|
||||
int dbg_search_packet( IOBUF inp, PACKET *pkt, int pkttype, ulong *retpos, const char* file, int lineno );
|
||||
int dbg_parse_packet( IOBUF inp, PACKET *ret_pkt, const char* file, int lineno );
|
||||
int dbg_copy_all_packets( IOBUF inp, IOBUF out, const char* file, int lineno );
|
||||
int dbg_copy_some_packets( IOBUF inp, IOBUF out, ulong stopoff, const char* file, int lineno );
|
||||
int dbg_skip_some_packets( IOBUF inp, unsigned n, const char* file, int lineno );
|
||||
#define search_packet( a,b,c,d ) dbg_search_packet( (a), (b), (c), (d), __FILE__, __LINE__ )
|
||||
#define parse_packet( a, b ) dbg_parse_packet( (a), (b), __FILE__, __LINE__ )
|
||||
#define copy_all_packets( a,b ) dbg_copy_all_packets((a),(b), __FILE__, __LINE__ )
|
||||
#define copy_some_packets( a,b,c ) dbg_copy_some_packets((a),(b),(c), __FILE__, __LINE__ )
|
||||
#define skip_some_packets( a,b ) dbg_skip_some_packets((a),(b), __FILE__, __LINE__ )
|
||||
int dbg_search_packet( IOBUF inp, PACKET *pkt, int pkttype, off_t *retpos,
|
||||
const char* file, int lineno );
|
||||
int dbg_parse_packet( IOBUF inp, PACKET *ret_pkt,
|
||||
const char* file, int lineno );
|
||||
int dbg_copy_all_packets( IOBUF inp, IOBUF out,
|
||||
const char* file, int lineno );
|
||||
int dbg_copy_some_packets( IOBUF inp, IOBUF out, off_t stopoff,
|
||||
const char* file, int lineno );
|
||||
int dbg_skip_some_packets( IOBUF inp, unsigned n,
|
||||
const char* file, int lineno );
|
||||
#define search_packet( a,b,c,d ) \
|
||||
dbg_search_packet( (a), (b), (c), (d), __FILE__, __LINE__ )
|
||||
#define parse_packet( a, b ) \
|
||||
dbg_parse_packet( (a), (b), __FILE__, __LINE__ )
|
||||
#define copy_all_packets( a,b ) \
|
||||
dbg_copy_all_packets((a),(b), __FILE__, __LINE__ )
|
||||
#define copy_some_packets( a,b,c ) \
|
||||
dbg_copy_some_packets((a),(b),(c), __FILE__, __LINE__ )
|
||||
#define skip_some_packets( a,b ) \
|
||||
dbg_skip_some_packets((a),(b), __FILE__, __LINE__ )
|
||||
#else
|
||||
int search_packet( IOBUF inp, PACKET *pkt, int pkttype, ulong *retpos );
|
||||
int search_packet( IOBUF inp, PACKET *pkt, int pkttype, off_t *retpos );
|
||||
int parse_packet( IOBUF inp, PACKET *ret_pkt);
|
||||
int copy_all_packets( IOBUF inp, IOBUF out );
|
||||
int copy_some_packets( IOBUF inp, IOBUF out, ulong stopoff );
|
||||
int copy_some_packets( IOBUF inp, IOBUF out, off_t stopoff );
|
||||
int skip_some_packets( IOBUF inp, unsigned n );
|
||||
#endif
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ static int mpi_print_mode = 0;
|
|||
static int list_mode = 0;
|
||||
|
||||
static int parse( IOBUF inp, PACKET *pkt, int reqtype,
|
||||
ulong *retpos, int *skip, IOBUF out, int do_skip
|
||||
off_t *retpos, int *skip, IOBUF out, int do_skip
|
||||
#ifdef DEBUG_PARSE_PACKET
|
||||
,const char *dbg_w, const char *dbg_f, int dbg_l
|
||||
#endif
|
||||
|
@ -158,7 +158,7 @@ parse_packet( IOBUF inp, PACKET *pkt )
|
|||
*/
|
||||
#ifdef DEBUG_PARSE_PACKET
|
||||
int
|
||||
dbg_search_packet( IOBUF inp, PACKET *pkt, int pkttype, ulong *retpos,
|
||||
dbg_search_packet( IOBUF inp, PACKET *pkt, int pkttype, off_t *retpos,
|
||||
const char *dbg_f, int dbg_l )
|
||||
{
|
||||
int skip, rc;
|
||||
|
@ -170,7 +170,7 @@ dbg_search_packet( IOBUF inp, PACKET *pkt, int pkttype, ulong *retpos,
|
|||
}
|
||||
#else
|
||||
int
|
||||
search_packet( IOBUF inp, PACKET *pkt, int pkttype, ulong *retpos )
|
||||
search_packet( IOBUF inp, PACKET *pkt, int pkttype, off_t *retpos )
|
||||
{
|
||||
int skip, rc;
|
||||
|
||||
|
@ -215,7 +215,7 @@ copy_all_packets( IOBUF inp, IOBUF out )
|
|||
*/
|
||||
#ifdef DEBUG_PARSE_PACKET
|
||||
int
|
||||
dbg_copy_some_packets( IOBUF inp, IOBUF out, ulong stopoff,
|
||||
dbg_copy_some_packets( IOBUF inp, IOBUF out, off_t stopoff,
|
||||
const char *dbg_f, int dbg_l )
|
||||
{
|
||||
PACKET pkt;
|
||||
|
@ -230,7 +230,7 @@ dbg_copy_some_packets( IOBUF inp, IOBUF out, ulong stopoff,
|
|||
}
|
||||
#else
|
||||
int
|
||||
copy_some_packets( IOBUF inp, IOBUF out, ulong stopoff )
|
||||
copy_some_packets( IOBUF inp, IOBUF out, off_t stopoff )
|
||||
{
|
||||
PACKET pkt;
|
||||
int skip, rc=0;
|
||||
|
@ -284,7 +284,7 @@ skip_some_packets( IOBUF inp, unsigned n )
|
|||
* if OUT is not NULL, a special copymode is used.
|
||||
*/
|
||||
static int
|
||||
parse( IOBUF inp, PACKET *pkt, int reqtype, ulong *retpos,
|
||||
parse( IOBUF inp, PACKET *pkt, int reqtype, off_t *retpos,
|
||||
int *skip, IOBUF out, int do_skip
|
||||
#ifdef DEBUG_PARSE_PACKET
|
||||
,const char *dbg_w, const char *dbg_f, int dbg_l
|
||||
|
@ -309,8 +309,7 @@ parse( IOBUF inp, PACKET *pkt, int reqtype, ulong *retpos,
|
|||
hdrlen=0;
|
||||
hdr[hdrlen++] = ctb;
|
||||
if( !(ctb & 0x80) ) {
|
||||
log_error("%s: invalid packet (ctb=%02x) near %lu\n",
|
||||
iobuf_where(inp), ctb, iobuf_tell(inp) );
|
||||
log_error("%s: invalid packet (ctb=%02x)\n", iobuf_where(inp), ctb );
|
||||
rc = G10ERR_INVALID_PACKET;
|
||||
goto leave;
|
||||
}
|
||||
|
|
231
g10/pipemode.c
Normal file
231
g10/pipemode.c
Normal file
|
@ -0,0 +1,231 @@
|
|||
/* pipemode.c - pipemode handler
|
||||
* Copyright (C) 2000 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GnuPG.
|
||||
*
|
||||
* GnuPG is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* GnuPG is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "options.h"
|
||||
#include "packet.h"
|
||||
#include "errors.h"
|
||||
#include "iobuf.h"
|
||||
#include "keydb.h"
|
||||
#include "memory.h"
|
||||
#include "util.h"
|
||||
#include "main.h"
|
||||
#include "status.h"
|
||||
#include "filter.h"
|
||||
|
||||
|
||||
#define CONTROL_PACKET_SPACE 30
|
||||
|
||||
enum pipemode_state_e {
|
||||
STX_init = 0,
|
||||
STX_wait_operation,
|
||||
STX_begin,
|
||||
STX_text,
|
||||
STX_wait_init
|
||||
};
|
||||
|
||||
|
||||
struct pipemode_context_s {
|
||||
enum pipemode_state_e state;
|
||||
int operation;
|
||||
int stop;
|
||||
};
|
||||
|
||||
|
||||
static size_t
|
||||
make_control ( byte *buf, int code, int operation )
|
||||
{
|
||||
const byte *sesmark;
|
||||
size_t sesmarklen, n=0;;
|
||||
|
||||
sesmark = get_session_marker( &sesmarklen );
|
||||
if ( sesmarklen > 20 )
|
||||
BUG();
|
||||
|
||||
buf[n++] = 0xff; /* new format, type 63, 1 length byte */
|
||||
n++; /* length will fixed below */
|
||||
memcpy(buf+n, sesmark, sesmarklen ); n+= sesmarklen;
|
||||
buf[n++] = 2; /* control type: pipemode marker */
|
||||
buf[n++] = code;
|
||||
buf[n++] = operation;
|
||||
buf[1] = n-2;
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
pipemode_filter( void *opaque, int control,
|
||||
IOBUF a, byte *buf, size_t *ret_len)
|
||||
{
|
||||
size_t size = *ret_len;
|
||||
struct pipemode_context_s *stx = opaque;
|
||||
int rc=0;
|
||||
size_t n = 0;
|
||||
int esc = 0;
|
||||
|
||||
if( control == IOBUFCTRL_UNDERFLOW ) {
|
||||
*ret_len = 0;
|
||||
/* reserve some space for one control packet */
|
||||
if ( size <= CONTROL_PACKET_SPACE )
|
||||
BUG();
|
||||
size -= CONTROL_PACKET_SPACE;
|
||||
|
||||
|
||||
while ( n < size ) {
|
||||
int c = iobuf_get (a);
|
||||
if (c == -1) {
|
||||
if ( stx->state != STX_init ) {
|
||||
log_error ("EOF encountered at wrong state\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ( esc ) {
|
||||
switch (c) {
|
||||
case '@':
|
||||
if ( stx->state == STX_text ) {
|
||||
buf[n++] = c;
|
||||
break;
|
||||
}
|
||||
log_error ("@@ not allowed in current state\n");
|
||||
return -1;
|
||||
case '<': /* begin of stream part */
|
||||
if ( stx->state != STX_init ) {
|
||||
log_error ("nested begin of stream\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
stx->state = STX_wait_operation;
|
||||
break;
|
||||
case '>': /* end of stream part */
|
||||
if ( stx->state != STX_wait_init ) {
|
||||
log_error ("invalid state for @>\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
stx->state = STX_init;
|
||||
break;
|
||||
case 'V': /* operation = verify */
|
||||
case 'E': /* operation = encrypt */
|
||||
case 'S': /* operation = sign */
|
||||
case 'B': /* operation = detach sign */
|
||||
case 'C': /* operation = clearsign */
|
||||
case 'D': /* operation = decrypt */
|
||||
if ( stx->state != STX_wait_operation ) {
|
||||
log_error ("invalid state for operation code\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
stx->operation = c;
|
||||
stx->state = STX_begin;
|
||||
n += make_control ( buf, 1, stx->operation );
|
||||
goto leave;
|
||||
|
||||
case 't': /* plaintext text follows */
|
||||
if ( stx->state != STX_begin ) {
|
||||
log_error ("invalid state for @t\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
if ( stx->operation != 'E' ) {
|
||||
log_error ("invalid operation for @t\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
stx->state = STX_text;
|
||||
n += make_control ( buf, 2, c );
|
||||
goto leave;
|
||||
|
||||
case '.': /* ready */
|
||||
if ( stx->state == STX_text )
|
||||
;
|
||||
else {
|
||||
log_error ("invalid state for @.\n");
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
stx->state = STX_wait_init;
|
||||
n += make_control ( buf, 3, c );
|
||||
goto leave;
|
||||
|
||||
default:
|
||||
log_error ("invalid escape sequence 0x%02x in stream\n",
|
||||
c);
|
||||
stx->stop = 1;
|
||||
return -1;
|
||||
}
|
||||
esc = 0;
|
||||
}
|
||||
else if (c == '@')
|
||||
esc = 1;
|
||||
else
|
||||
buf[n++] = c;
|
||||
}
|
||||
|
||||
leave:
|
||||
if ( !n ) {
|
||||
stx->stop = 1;
|
||||
rc = -1; /* eof */
|
||||
}
|
||||
*ret_len = n;
|
||||
}
|
||||
else if( control == IOBUFCTRL_DESC )
|
||||
*(char**)buf = "pipemode_filter";
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void
|
||||
run_in_pipemode(void)
|
||||
{
|
||||
IOBUF fp;
|
||||
armor_filter_context_t afx;
|
||||
struct pipemode_context_s stx;
|
||||
int rc;
|
||||
|
||||
memset( &afx, 0, sizeof afx);
|
||||
memset( &stx, 0, sizeof stx);
|
||||
|
||||
fp = iobuf_open("-");
|
||||
iobuf_push_filter (fp, pipemode_filter, &stx );
|
||||
|
||||
if( !opt.no_armor )
|
||||
iobuf_push_filter( fp, armor_filter, &afx );
|
||||
|
||||
do {
|
||||
log_debug ("pipemode: begin proc_packets\n");
|
||||
rc = proc_packets( NULL, fp );
|
||||
log_debug ("pipemode: end proc_packets: %s\n", g10_errstr (rc));
|
||||
} while ( !stx.stop );
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1106,7 +1106,7 @@ keyring_search( PACKET *req, KBPOS *kbpos, IOBUF iobuf, const char *fname )
|
|||
int rc;
|
||||
PACKET pkt;
|
||||
int save_mode;
|
||||
ulong offset;
|
||||
off_t offset;
|
||||
int pkttype = req->pkttype;
|
||||
PKT_public_key *req_pk = req->pkt.public_key;
|
||||
PKT_secret_key *req_sk = req->pkt.secret_key;
|
||||
|
@ -1188,9 +1188,9 @@ keyring_read( KBPOS *kbpos, KBNODE *ret_root )
|
|||
}
|
||||
|
||||
if( !kbpos->valid )
|
||||
log_debug("kbpos not valid in keyring_read, want %d\n", (int)kbpos->offset );
|
||||
log_debug("kbpos not valid in keyring_read\n" );
|
||||
if( iobuf_seek( a, kbpos->offset ) ) {
|
||||
log_error("can't seek to %lu\n", kbpos->offset);
|
||||
log_error("can't seek\n");
|
||||
iobuf_close(a);
|
||||
return G10ERR_KEYRING_OPEN;
|
||||
}
|
||||
|
|
|
@ -161,4 +161,3 @@ verify_files( int nfiles, char **files )
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue