mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-07-01 16:58:30 +00:00
Add MODE=optlinux build mode (#141)
This commit is contained in:
parent
226aaf3547
commit
67b5200a0b
111 changed files with 934 additions and 854 deletions
|
@ -19,9 +19,12 @@
|
|||
#include "libc/calls/math.h"
|
||||
#include "libc/macros.internal.h"
|
||||
|
||||
/**
|
||||
* Adds resource usages.
|
||||
*/
|
||||
void AddRusage(struct rusage *x, const struct rusage *y) {
|
||||
AddTimeval(&x->ru_utime, &y->ru_utime);
|
||||
AddTimeval(&x->ru_stime, &y->ru_stime);
|
||||
x->ru_utime = AddTimeval(x->ru_utime, y->ru_utime);
|
||||
x->ru_stime = AddTimeval(x->ru_stime, y->ru_stime);
|
||||
x->ru_maxrss = MAX(x->ru_maxrss, y->ru_maxrss);
|
||||
x->ru_ixrss += y->ru_ixrss;
|
||||
x->ru_idrss += y->ru_idrss;
|
||||
|
|
32
libc/calls/addtimespec.c
Normal file
32
libc/calls/addtimespec.c
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||
│ │
|
||||
│ Permission to use, copy, modify, and/or 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. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/calls/math.h"
|
||||
|
||||
/**
|
||||
* Adds two microsecond timestamps.
|
||||
*/
|
||||
struct timespec AddTimespec(struct timespec x, struct timespec y) {
|
||||
x.tv_sec += y.tv_sec;
|
||||
x.tv_nsec += y.tv_nsec;
|
||||
if (x.tv_nsec >= 10000000000) {
|
||||
x.tv_nsec -= 10000000000;
|
||||
x.tv_sec += 1;
|
||||
}
|
||||
return x;
|
||||
}
|
|
@ -18,11 +18,15 @@
|
|||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/calls/math.h"
|
||||
|
||||
void AddTimeval(struct timeval *x, const struct timeval *y) {
|
||||
x->tv_sec += y->tv_sec;
|
||||
x->tv_usec += y->tv_usec;
|
||||
if (x->tv_usec >= 1000000) {
|
||||
x->tv_usec -= 1000000;
|
||||
x->tv_sec += 1;
|
||||
/**
|
||||
* Adds two microsecond timestamps.
|
||||
*/
|
||||
struct timeval AddTimeval(struct timeval x, struct timeval y) {
|
||||
x.tv_sec += y.tv_sec;
|
||||
x.tv_usec += y.tv_usec;
|
||||
if (x.tv_usec >= 1000000) {
|
||||
x.tv_usec -= 1000000;
|
||||
x.tv_sec += 1;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "libc/bits/weaken.h"
|
||||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/sysdebug.internal.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/intrin/asan.internal.h"
|
||||
#include "libc/sysv/errfuns.h"
|
||||
|
@ -39,9 +40,16 @@
|
|||
* @see fchmod()
|
||||
*/
|
||||
int fchmodat(int dirfd, const char *path, uint32_t mode, int flags) {
|
||||
int rc;
|
||||
if (IsAsan() && !__asan_is_valid(path, 1)) return efault();
|
||||
if (weaken(__zipos_notat) && weaken(__zipos_notat)(dirfd, path) == -1) {
|
||||
return -1; /* TODO(jart): implement me */
|
||||
rc = -1; /* TODO(jart): implement me */
|
||||
} else if (!IsWindows()) {
|
||||
rc = sys_fchmodat(dirfd, path, mode, flags);
|
||||
} else {
|
||||
rc = sys_fchmodat_nt(dirfd, path, mode, flags);
|
||||
}
|
||||
return sys_fchmodat(dirfd, path, mode, flags);
|
||||
SYSDEBUG("fchmodat(%d, %s, %o, %d) -> %d %s", (long)dirfd, path, mode, flags,
|
||||
rc != -1 ? "" : strerror(errno));
|
||||
return rc;
|
||||
}
|
||||
|
|
|
@ -44,30 +44,35 @@
|
|||
*/
|
||||
bool fileexists(const char *path) {
|
||||
int e;
|
||||
bool res;
|
||||
union metastat st;
|
||||
struct ZiposUri zipname;
|
||||
uint16_t path16[PATH_MAX];
|
||||
e = errno;
|
||||
if (IsAsan() && !__asan_is_valid(path, 1)) return efault();
|
||||
if (weaken(__zipos_open) && weaken(__zipos_parseuri)(path, &zipname) != -1) {
|
||||
e = errno;
|
||||
if (weaken(__zipos_stat)(&zipname, &st.cosmo) != -1) {
|
||||
return true;
|
||||
res = true;
|
||||
} else {
|
||||
errno = e;
|
||||
return false;
|
||||
res = false;
|
||||
}
|
||||
} else if (IsMetal()) {
|
||||
return false;
|
||||
res = false;
|
||||
} else if (!IsWindows()) {
|
||||
e = errno;
|
||||
if (__sys_fstatat(AT_FDCWD, path, &st, 0) != -1) {
|
||||
return true;
|
||||
res = true;
|
||||
} else {
|
||||
errno = e;
|
||||
return false;
|
||||
res = false;
|
||||
}
|
||||
} else if (__mkntpath(path, path16) != -1) {
|
||||
res = GetFileAttributes(path16) != -1u;
|
||||
} else {
|
||||
if (__mkntpath(path, path16) == -1) return -1;
|
||||
return GetFileAttributes(path16) != -1u;
|
||||
res = false;
|
||||
}
|
||||
SYSDEBUG("fileexists(%s) -> %s %s", path, res ? "true" : "false",
|
||||
res ? "" : strerror(errno));
|
||||
if (!res && (errno == ENOENT || errno == ENOTDIR)) {
|
||||
errno = e;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
#include "libc/calls/weirdtypes.h"
|
||||
#include "libc/str/str.h"
|
||||
|
||||
/**
|
||||
* Convert pathname and a project ID to System V IPC key.
|
||||
*/
|
||||
int ftok(const char *path, int id) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == -1) return -1;
|
||||
|
|
|
@ -16,8 +16,12 @@
|
|||
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/dce.h"
|
||||
#include "libc/runtime/runtime.h"
|
||||
|
||||
#define ToUpper(c) \
|
||||
(IsWindows() && (c) >= 'a' && (c) <= 'z' ? (c) - 'a' + 'A' : (c))
|
||||
|
||||
/**
|
||||
* Returns value of environment variable, or NULL if not found.
|
||||
*
|
||||
|
@ -35,11 +39,11 @@ char *getenv(const char *s) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
if (s[j] != p[i][j]) {
|
||||
if (ToUpper(s[j]) != ToUpper(p[i][j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/struct/metastat.internal.h"
|
||||
#include "libc/calls/struct/stat.h"
|
||||
#include "libc/calls/sysdebug.internal.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/intrin/asan.internal.h"
|
||||
|
@ -43,29 +44,34 @@
|
|||
* @see isregularfile(), issymlink(), ischardev()
|
||||
*/
|
||||
bool isdirectory(const char *path) {
|
||||
int rc, e;
|
||||
int e;
|
||||
bool res;
|
||||
union metastat st;
|
||||
struct ZiposUri zipname;
|
||||
e = errno;
|
||||
if (IsAsan() && !__asan_is_valid(path, 1)) return efault();
|
||||
if (weaken(__zipos_open) && weaken(__zipos_parseuri)(path, &zipname) != -1) {
|
||||
e = errno;
|
||||
if (weaken(__zipos_stat)(&zipname, &st.cosmo) != -1) {
|
||||
return S_ISDIR(st.cosmo.st_mode);
|
||||
res = S_ISDIR(st.cosmo.st_mode);
|
||||
} else {
|
||||
errno = e;
|
||||
return false;
|
||||
res = false;
|
||||
}
|
||||
} else if (IsMetal()) {
|
||||
return false;
|
||||
res = false;
|
||||
} else if (!IsWindows()) {
|
||||
e = errno;
|
||||
if (__sys_fstatat(AT_FDCWD, path, &st, AT_SYMLINK_NOFOLLOW) != -1) {
|
||||
return S_ISDIR(METASTAT(st, st_mode));
|
||||
res = S_ISDIR(METASTAT(st, st_mode));
|
||||
} else {
|
||||
errno = e;
|
||||
return false;
|
||||
res = false;
|
||||
}
|
||||
} else {
|
||||
return isdirectory_nt(path);
|
||||
res = isdirectory_nt(path);
|
||||
}
|
||||
SYSDEBUG("isdirectory(%s) -> %s %s", path, res ? "true" : "false",
|
||||
res ? "" : strerror(errno));
|
||||
if (!res && (errno == ENOENT || errno == ENOTDIR)) {
|
||||
errno = e;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
#ifndef COSMOPOLITAN_LIBC_CALLS_MATH_H_
|
||||
#define COSMOPOLITAN_LIBC_CALLS_MATH_H_
|
||||
#include "libc/calls/struct/rusage.h"
|
||||
#include "libc/calls/struct/timespec.h"
|
||||
#include "libc/calls/struct/timeval.h"
|
||||
#if !(__ASSEMBLER__ + __LINKER__ + 0)
|
||||
COSMOPOLITAN_C_START_
|
||||
|
||||
void AddTimeval(struct timeval *, const struct timeval *);
|
||||
struct timeval AddTimeval(struct timeval, struct timeval);
|
||||
struct timespec AddTimespec(struct timespec, struct timespec);
|
||||
void AddRusage(struct rusage *, const struct rusage *);
|
||||
|
||||
COSMOPOLITAN_C_END_
|
||||
|
|
|
@ -29,10 +29,13 @@
|
|||
#include "libc/str/utf16.h"
|
||||
#include "libc/sysv/errfuns.h"
|
||||
|
||||
#define ToUpper(c) ((c) >= 'a' && (c) <= 'z' ? (c) - 'a' + 'A' : (c))
|
||||
|
||||
static noasan int CompareStrings(const char *l, const char *r) {
|
||||
int a, b;
|
||||
size_t i = 0;
|
||||
while (l[i] == r[i] && r[i]) ++i;
|
||||
return (l[i] & 0xff) - (r[i] & 0xff);
|
||||
while ((a = ToUpper(l[i] & 255)) == (b = ToUpper(r[i] & 255)) && r[i]) ++i;
|
||||
return a - b;
|
||||
}
|
||||
|
||||
static noasan void InsertString(char **a, size_t i, char *s) {
|
||||
|
@ -56,6 +59,7 @@ static noasan void InsertString(char **a, size_t i, char *s) {
|
|||
*/
|
||||
textwindows noasan int mkntenvblock(char16_t envvars[ARG_MAX],
|
||||
char *const envp[], const char *extravar) {
|
||||
bool v;
|
||||
char *t;
|
||||
axdx_t rc;
|
||||
uint64_t w;
|
||||
|
@ -68,6 +72,7 @@ textwindows noasan int mkntenvblock(char16_t envvars[ARG_MAX],
|
|||
if (extravar) InsertString(vars, n++, extravar);
|
||||
for (k = i = 0; i < n; ++i) {
|
||||
j = 0;
|
||||
v = false;
|
||||
do {
|
||||
x = vars[i][j++] & 0xff;
|
||||
if (x >= 0200) {
|
||||
|
@ -83,6 +88,13 @@ textwindows noasan int mkntenvblock(char16_t envvars[ARG_MAX],
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!v) {
|
||||
if (x != '=') {
|
||||
x = ToUpper(x);
|
||||
} else {
|
||||
v = true;
|
||||
}
|
||||
}
|
||||
w = EncodeUtf16(x);
|
||||
do {
|
||||
envvars[k++] = w & 0xffff;
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/sysdebug.internal.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/str/str.h"
|
||||
|
@ -59,12 +58,5 @@ int sys_openat(int dirfd, const char *file, int flags, unsigned mode) {
|
|||
d = sys_openat(dirfd, file, flags, mode);
|
||||
}
|
||||
}
|
||||
if (d != -1) {
|
||||
SYSDEBUG("sys_openat(%d, %s, %d, %d) -> %d", (long)dirfd, file, flags,
|
||||
(flags & (O_CREAT | O_TMPFILE)) ? mode : 0, d);
|
||||
} else {
|
||||
SYSDEBUG("sys_openat(%d, %s, %d, %d) -> %s", (long)dirfd, file, flags,
|
||||
(flags & (O_CREAT | O_TMPFILE)) ? mode : 0, strerror(errno));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "libc/bits/weaken.h"
|
||||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/sysdebug.internal.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/intrin/asan.internal.h"
|
||||
#include "libc/log/log.h"
|
||||
|
@ -43,24 +44,37 @@
|
|||
* @vforksafe
|
||||
*/
|
||||
int openat(int dirfd, const char *file, int flags, ...) {
|
||||
int rc;
|
||||
va_list va;
|
||||
unsigned mode;
|
||||
struct ZiposUri zipname;
|
||||
va_start(va, flags);
|
||||
mode = va_arg(va, unsigned);
|
||||
va_end(va);
|
||||
if (!file) return efault();
|
||||
if (IsAsan() && !__asan_is_valid(file, 1)) return efault();
|
||||
if (__isfdkind(dirfd, kFdZip)) return einval(); /* TODO(jart): implement me */
|
||||
if (weaken(__zipos_open) && weaken(__zipos_parseuri)(file, &zipname) != -1) {
|
||||
if (__vforked) return eopnotsupp();
|
||||
if (dirfd != AT_FDCWD) return eopnotsupp();
|
||||
return weaken(__zipos_open)(&zipname, flags, mode);
|
||||
} else if (!IsWindows() && !IsMetal()) {
|
||||
return sys_openat(dirfd, file, flags, mode);
|
||||
} else if (IsMetal()) {
|
||||
return sys_openat_metal(dirfd, file, flags, mode);
|
||||
if (file && (!IsAsan() || __asan_is_valid(file, 1))) {
|
||||
if (!__isfdkind(dirfd, kFdZip)) {
|
||||
if (weaken(__zipos_open) &&
|
||||
weaken(__zipos_parseuri)(file, &zipname) != -1) {
|
||||
if (!__vforked && dirfd == AT_FDCWD) {
|
||||
rc = weaken(__zipos_open)(&zipname, flags, mode);
|
||||
} else {
|
||||
rc = eopnotsupp(); /* TODO */
|
||||
}
|
||||
} else if (!IsWindows() && !IsMetal()) {
|
||||
rc = sys_openat(dirfd, file, flags, mode);
|
||||
} else if (IsMetal()) {
|
||||
rc = sys_openat_metal(dirfd, file, flags, mode);
|
||||
} else {
|
||||
rc = sys_open_nt(dirfd, file, flags, mode);
|
||||
}
|
||||
} else {
|
||||
rc = eopnotsupp(); /* TODO */
|
||||
}
|
||||
} else {
|
||||
return sys_open_nt(dirfd, file, flags, mode);
|
||||
rc = efault();
|
||||
}
|
||||
SYSDEBUG("openat(%d, %s, %d, %d) -> %d %s", (long)dirfd, file, flags,
|
||||
(flags & (O_CREAT | O_TMPFILE)) ? mode : 0, (long)rc,
|
||||
rc == -1 ? strerror(errno) : "");
|
||||
return rc;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,16 @@
|
|||
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/bits/weaken.h"
|
||||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/struct/iovec.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/intrin/asan.internal.h"
|
||||
#include "libc/sock/internal.h"
|
||||
#include "libc/sock/sock.h"
|
||||
#include "libc/sysv/errfuns.h"
|
||||
#include "libc/zipos/zipos.internal.h"
|
||||
|
||||
/**
|
||||
* Reads data from file descriptor.
|
||||
|
@ -32,5 +39,22 @@
|
|||
* @asyncsignalsafe
|
||||
*/
|
||||
ssize_t read(int fd, void *buf, size_t size) {
|
||||
return readv(fd, &(struct iovec){buf, size}, 1);
|
||||
if (fd >= 0) {
|
||||
if (IsAsan() && !__asan_is_valid(buf, size)) return efault();
|
||||
if (fd < g_fds.n && g_fds.p[fd].kind == kFdZip) {
|
||||
return weaken(__zipos_read)(
|
||||
(struct ZiposHandle *)(intptr_t)g_fds.p[fd].handle,
|
||||
&(struct iovec){buf, size}, 1, -1);
|
||||
} else if (!IsWindows() && !IsMetal()) {
|
||||
return sys_read(fd, buf, size);
|
||||
} else if (fd >= g_fds.n) {
|
||||
return ebadf();
|
||||
} else if (IsMetal()) {
|
||||
return sys_readv_metal(g_fds.p + fd, &(struct iovec){buf, size}, 1);
|
||||
} else {
|
||||
return sys_readv_nt(g_fds.p + fd, &(struct iovec){buf, size}, 1);
|
||||
}
|
||||
} else {
|
||||
return einval();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ noasan int sigaltstack(const struct sigaltstack *neu, struct sigaltstack *old) {
|
|||
return enosys();
|
||||
}
|
||||
if ((rc = sys_sigaltstack(a, b)) != -1) {
|
||||
if (old) {
|
||||
if (IsBsd() && old) {
|
||||
sigaltstack2linux(old, &bsd);
|
||||
}
|
||||
return 0;
|
||||
|
|
|
@ -17,9 +17,13 @@
|
|||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/bits/weaken.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/mem/internal.h"
|
||||
#include "libc/runtime/runtime.h"
|
||||
|
||||
#define ToUpper(c) \
|
||||
(IsWindows() && (c) >= 'a' && (c) <= 'z' ? (c) - 'a' + 'A' : (c))
|
||||
|
||||
/**
|
||||
* Removes environment variable.
|
||||
*/
|
||||
|
@ -42,7 +46,7 @@ int unsetenv(const char *s) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
if (s[j] != p[i][j]) {
|
||||
if (ToUpper(s[j]) != ToUpper(p[i][j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,14 @@
|
|||
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/bits/weaken.h"
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/struct/iovec.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/intrin/asan.internal.h"
|
||||
#include "libc/sock/sock.h"
|
||||
#include "libc/sysv/errfuns.h"
|
||||
#include "libc/zipos/zipos.internal.h"
|
||||
|
||||
/**
|
||||
* Writes data to file descriptor.
|
||||
|
@ -31,5 +37,22 @@
|
|||
* @asyncsignalsafe
|
||||
*/
|
||||
ssize_t write(int fd, const void *buf, size_t size) {
|
||||
return writev(fd, &(struct iovec){buf, size}, 1);
|
||||
if (fd >= 0) {
|
||||
if (IsAsan() && !__asan_is_valid(buf, size)) return efault();
|
||||
if (fd < g_fds.n && g_fds.p[fd].kind == kFdZip) {
|
||||
return weaken(__zipos_write)(
|
||||
(struct ZiposHandle *)(intptr_t)g_fds.p[fd].handle,
|
||||
&(struct iovec){buf, size}, 1, -1);
|
||||
} else if (!IsWindows() && !IsMetal()) {
|
||||
return sys_write(fd, buf, size);
|
||||
} else if (fd >= g_fds.n) {
|
||||
return ebadf();
|
||||
} else if (IsMetal()) {
|
||||
return sys_writev_metal(g_fds.p + fd, &(struct iovec){buf, size}, 1);
|
||||
} else {
|
||||
return sys_writev_nt(g_fds.p + fd, &(struct iovec){buf, size}, 1);
|
||||
}
|
||||
} else {
|
||||
return einval();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue