diff --git a/Tmain/oneshot.d/run.sh b/Tmain/oneshot.d/run.sh new file mode 100644 index 0000000000..467c1a4326 --- /dev/null +++ b/Tmain/oneshot.d/run.sh @@ -0,0 +1,19 @@ +# Copyright: 2026 Masatake YAMATO +# License: GPL-2 + +CTAGS=$1 + +gen() +{ + cat <`` + Makes ctags behave as a filter, reading source + file contents from standard input and printing their tags to + standard output. When this option is enabled, the options ``-L``, + ``-f``, ``-o``, and ``--totals`` are ignored. + + ctags assumes ** is the input file + name of the contents. It affects language selection. + ** appears as the input file name in the tags output. + + This option is useful for extracting interesting names in command + output. + + For example, you can extract C preprocessor macro names in the source + code specified by a URL: + + .. code-block:: console + + $ curl -s -o - \ + 'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \ + | ./ctags --oneshot=buf.c --kinds-C='{macro}' + BH_ENTRY buf.c /^#define BH_ENTRY(/;" d file: + ... + + Make the output in JSON format: + + .. code-block:: console + + $ curl -s -o - \ + 'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \ + | ./ctags --oneshot=buf.c --kinds-C='{macro}' --output-format=json \ + | jq . + { + "_type": "tag", + "name": "BH_ENTRY", + "path": "buf.c", + "pattern": "/^#define BH_ENTRY(/", + "file": true, + "kind": "macro" + } + ... + ``--links[=(yes|no)]`` Indicates whether symbolic links (if supported) should be followed. When disabled, symbolic links are ignored. This option is on by default. diff --git a/docs/news/HEAD.rst b/docs/news/HEAD.rst index 83cf9670ba..408e207c64 100644 --- a/docs/news/HEAD.rst +++ b/docs/news/HEAD.rst @@ -27,6 +27,12 @@ Extend ``--map-=`` and ``--langmap=`` options to choose a parser using reg ``--list-maps`` has also been extended to include regular-expression-based mappings. +New option: ``--oneshot=`` + + Makes @CTAGS_NAME_EXECUTABLE@ behave as a filter, reading source + file contents from standard input and printing their tags to + standard output. + Incompatible changes --------------------------------------------------------------------- Messages for broken symlinks are now emitted at NOTICE level instead of diff --git a/main/entry.c b/main/entry.c index 0d4f4eb802..673be91d69 100644 --- a/main/entry.c +++ b/main/entry.c @@ -409,7 +409,7 @@ extern void openTagFile (void) */ if (TagsToStdout) { - if (Option.interactive == INTERACTIVE_SANDBOX) + if (Option.interactive & INTERACTIVE_WITH_SANDBOX) { TagFile.mio = mio_new_memory (NULL, 0, eRealloc, eFreeNoNullCheck); TagFile.name = NULL; @@ -536,7 +536,6 @@ static int replacementTruncate (const char *const name, const long size) #endif -#ifndef EXTERNAL_SORT static void internalSortTagFile (void) { MIO *mio; @@ -562,20 +561,25 @@ static void internalSortTagFile (void) if (! TagsToStdout) mio_unref (mio); } -#endif -static void sortTagFile (void) +static void sortTagFile (bool forceUseInternalSort) { if (TagFile.numTags.added > 0L) { if (Option.sorted != SO_UNSORTED) { verbose ("sorting tag file\n"); + + if (forceUseInternalSort) + internalSortTagFile (); + else + { #ifdef EXTERNAL_SORT - externalSortTags (TagsToStdout, TagFile.mio); + externalSortTags (TagsToStdout, TagFile.mio); #else - internalSortTagFile (); + internalSortTagFile (); #endif + } } else if (TagsToStdout) catFile (TagFile.mio); @@ -632,7 +636,7 @@ static void writeEtagsIncludes (MIO *const mio) } } -extern void closeTagFile (const bool resize) +extern void closeTagFile (const bool resize, bool forceUseInternalSort) { long desiredSize, size; @@ -656,7 +660,7 @@ extern void closeTagFile (const bool resize) TagFile.name? TagFile.name: "", size, desiredSize); ) resizeTagFile (desiredSize); } - sortTagFile (); + sortTagFile (forceUseInternalSort); if (TagsToStdout) { if (mio_unref (TagFile.mio) != 0) diff --git a/main/entry_p.h b/main/entry_p.h index 3809294de8..cd9df947ca 100644 --- a/main/entry_p.h +++ b/main/entry_p.h @@ -29,7 +29,7 @@ extern const roleDefinition* getTagRole(const tagEntryInfo *const tag, int roleI extern void freeTagFileResources (void); extern const char *tagFileName (void); extern void openTagFile (void); -extern void closeTagFile (const bool resize); +extern void closeTagFile (const bool resize, bool forceUseInternalSort); extern void setupWriter (void *writerClientData); extern bool teardownWriter (const char *inputFilename); diff --git a/main/interactive_p.h b/main/interactive_p.h index 6dbd5be24b..9aa227f891 100644 --- a/main/interactive_p.h +++ b/main/interactive_p.h @@ -17,12 +17,30 @@ struct interactiveModeArgs { bool sandbox; + const char *fname; /* --_interactive=oneshot:FNAME */ + size_t limit; /* Upper limit of input data + * received via stdin. + * Used in oneshot mode. + * 0 means no limit. */ }; +#ifdef HAVE_JANSSON void interactiveLoop (cookedArgs *args, void *user); bool jsonErrorPrinter (const errorSelection selection, const char *const format, va_list ap, void *data); -int installSyscallFilter (void); +#endif + +void interactiveOneshot (cookedArgs *args, void *user); +void batchOneshot (cookedArgs *args, void *user); + +enum syscallSet { + syscall_coreset = 1 << 0, + syscall_open = 1 << 1, + syscall_close = 1 << 2, + syscall_ctrlset = 1 << 3, +}; + +int installSyscallFilter (unsigned int set); #endif /* CTAGS_MAIN_INTERACTIVE_H */ diff --git a/main/main.c b/main/main.c index f71d8554ea..149363c119 100644 --- a/main/main.c +++ b/main/main.c @@ -67,8 +67,8 @@ #include "writer_p.h" #include "xtag_p.h" -#ifdef HAVE_JANSSON #include "interactive_p.h" +#ifdef HAVE_JANSSON #include #include #endif @@ -376,7 +376,7 @@ static void batchMakeTags (cookedArgs *args, void *user CTAGS_ATTR_UNUSED) timeStamp (1); if ((! Option.filter) && (!Option.printLanguage)) - closeTagFile (resize); + closeTagFile (resize, false); timeStamp (2); @@ -391,27 +391,24 @@ static void batchMakeTags (cookedArgs *args, void *user CTAGS_ATTR_UNUSED) #undef timeStamp } +static void prepareSandbox (unsigned int set) +{ + if (installSyscallFilter (set)) { + error (FATAL, "install_syscall_filter failed"); + /* The explicit exit call is needed because + "error (FATAL,..." just prints a message in + interactive mode. */ + exit (1); + } +} + #ifdef HAVE_JANSSON void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) { struct interactiveModeArgs *iargs = user; - if (iargs->sandbox) { - /* As of jansson 2.6, the object hashing is seeded off - of /dev/urandom, so trigger the hash seeding - before installing the syscall filter. - */ - json_t * tmp = json_object (); - json_decref (tmp); - - if (installSyscallFilter ()) { - error (FATAL, "install_syscall_filter failed"); - /* The explicit exit call is needed because - "error (FATAL,..." just prints a message in - interactive mode. */ - exit (1); - } - } + if (iargs->sandbox) + prepareSandbox (syscall_coreset); char buffer[1024]; json_t *request; @@ -457,7 +454,7 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) if (iargs->sandbox) { error (FATAL, "invalid request in sandbox submode: reading file contents from a file is limited"); - closeTagFile (false); + closeTagFile (false, false); goto next; } @@ -472,7 +469,7 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) mio_unref (mio); } - closeTagFile (false); + closeTagFile (false, false); fputs ("{\"_type\": \"completed\", \"command\": \"generate-tags\"}\n", stdout); fflush(stdout); } @@ -488,6 +485,68 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) } #endif +static void oneshotCommon (const char *fname, size_t limit, bool sandbox, int strictSyscallset) +{ + openTagFile (); + + if (sandbox && strictSyscallset) + prepareSandbox (strictSyscallset); + + MIO *mio = mio_new_memory (NULL, 0, eRealloc, eFreeNoNullCheck); + int c; + size_t r = 0; + while ((c = getchar()) != EOF) + { + r++; + if (limit != 0 && r > limit) + { + error (WARNING, "input read from stdin exceeds the limit: name: %s, size: %lu", + fname, + (unsigned long) limit); + break; + } + mio_putc (mio, c); + } + + mio_seek (mio, 0, SEEK_SET); + + parseFileWithMio (fname, mio, NULL); + + mio_unref (mio); + + closeTagFile (false, sandbox ? true : false); +} + +extern void batchOneshot (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) +{ + struct interactiveModeArgs *iargs = user; + + Assert (iargs->fname); + + unsigned int extra_set; + + extra_set = isDestinationStdout () ? syscall_close | syscall_ctrlset + : syscall_open | syscall_close | syscall_ctrlset; + if (iargs->sandbox) + prepareSandbox (syscall_coreset | extra_set); + + extra_set = isDestinationStdout () ? 0 + : syscall_open | syscall_close; + oneshotCommon (iargs->fname, iargs->limit, iargs->sandbox, syscall_coreset | extra_set); +} + +extern void interactiveOneshot (cookedArgs *args CTAGS_ATTR_UNUSED, void *user) +{ + struct interactiveModeArgs *iargs = user; + + Assert (iargs->fname); + + if (iargs->sandbox) + prepareSandbox (syscall_coreset); + + oneshotCommon (iargs->fname, iargs->limit, iargs->sandbox, 0); +} + static bool isSafeVar (const char* var) { const char *safe_vars[] = { diff --git a/main/options.c b/main/options.c index b0731c023d..7fa62b6251 100644 --- a/main/options.c +++ b/main/options.c @@ -176,7 +176,7 @@ optionValues Option = { .patternLengthLimit = 96, .putFieldPrefix = false, .maxRecursionDepth = 0xffffffff, - .interactive = false, + .interactive = INTERACTIVE_NONE, .fieldsReset = false, #ifdef _WIN32 .useSlashAsFilenameSeparator = FILENAME_SEP_UNSET, @@ -186,6 +186,8 @@ optionValues Option = { #endif }; +static size_t oneshotLimit = 32 * 1024 * 1024; /* 32MB */ + struct localOptionValues { bool machinable; /* --machinable */ bool withListHeader; /* --with-list-header */ @@ -225,6 +227,9 @@ static optionDescription LongOptionDescription [] = { {1,0," --filter-terminator="}, {1,0," Specify to print to stdout following the tags for each file"}, {1,0," parsed when --filter is enabled."}, + {1,0," --oneshot="}, + {1,0," Behave as a filter, reading file contents from standard input and"}, + {1,0," writing tags to standard output. is used as the input field of tags."}, {1,0," --links[=(yes|no)]"}, {1,0," Indicate whether symbolic links should be followed [yes]."}, {1,0," --maxdepth="}, @@ -494,6 +499,8 @@ static optionDescription LongOptionDescription [] = { {1,0," Print this option summary including experimental features."}, {1,0," --license"}, {1,0," Print details of software license."}, + {1,0," --oneshot-limit="}, + {1,0," Limit the input size in the oneshot mode, in bytes (default is 32MB)."}, {0,0," --print-language"}, {0,0," Don't make tags file but just print the guessed language name for"}, {0,0," input file."}, @@ -743,7 +750,7 @@ extern void freeList (stringList** const pList) extern void setDefaultTagFileName (void) { - if (Option.filter || Option.interactive) + if (Option.filter || (Option.interactive & INTERACTIVE_MODE)) return; if (Option.tagFileName == NULL) @@ -882,8 +889,11 @@ static void setXrefMode (void) } #ifdef HAVE_JANSSON -static void setJsonMode (void) +static void setJsonMode (bool useJsonEvenInErrorReporting) { + if (useJsonEvenInErrorReporting) + setErrorPrinter (jsonErrorPrinter, NULL); + enablePtag (PTAG_JSON_OUTPUT_VERSION, true); enablePtag (PTAG_OUTPUT_MODE, false); enablePtag (PTAG_FILE_FORMAT, false); @@ -1673,50 +1683,90 @@ static void processHelpFullOption ( exit (0); } +static void processOneshot ( + const char *const option, + const char *const parameter) +{ + if (!parameter || parameter[0] == '\0') + error (FATAL, "--%s option requires a non-empty ", option); + + static struct interactiveModeArgs args; + Option.interactive = INTERACTIVE_ONESHOT; + +#ifdef HAVE_SECCOMP + Option.interactive |= INTERACTIVE_WITH_SANDBOX; +#endif + + args.fname = parameter; + args.limit = oneshotLimit; + args.sandbox = (Option.interactive & INTERACTIVE_WITH_SANDBOX); + + setMainLoop (batchOneshot, &args); +} + #ifdef HAVE_JANSSON static void processInteractiveOption ( - const char *const option CTAGS_ATTR_UNUSED, + const char *const option, const char *const parameter) { + void (* loop) (cookedArgs *args, void *user) = interactiveLoop; static struct interactiveModeArgs args; + args.sandbox = false; + args.fname = NULL; + args.limit = 0; if (parameter && (strcmp (parameter, "sandbox") == 0)) + Option.interactive = INTERACTIVE_MODE|INTERACTIVE_WITH_SANDBOX; + else if (parameter == NULL || *parameter == '\0' + || (parameter && strcmp (parameter, "default") == 0)) + Option.interactive = INTERACTIVE_MODE; + else { - Option.interactive = INTERACTIVE_SANDBOX; - args.sandbox = true; - } - else if (parameter && (strcmp (parameter, "default") == 0)) - { - Option.interactive = INTERACTIVE_DEFAULT; - args.sandbox = false; - } - else if ((!parameter) || *parameter == '\0') - { - Option.interactive = INTERACTIVE_DEFAULT; - args.sandbox = false; + const char * p = parameter; + if (p && (strncmp (p, "sandbox,", strlen ("sandbox,")) == 0)) + { + Option.interactive = INTERACTIVE_MODE|INTERACTIVE_WITH_SANDBOX; + p += strlen ("sandbox,"); + } + + if (p && (strncmp (p, "oneshot:", strlen ("oneshot:")) == 0)) + { + const char *fname = p + strlen ("oneshot:"); + + if (fname[0] == '\0') + error (FATAL, "--%s option requires a non-empty ", option); + + Option.interactive |= INTERACTIVE_MODE|INTERACTIVE_ONESHOT; + args.fname = fname; + args.limit = oneshotLimit; + } } - else + + if (! (Option.interactive & INTERACTIVE_MODE)) error (FATAL, "Unknown option argument \"%s\" for --%s option", parameter, option); + if (Option.interactive & INTERACTIVE_WITH_SANDBOX) + { #ifndef HAVE_SECCOMP - if (args.sandbox) error (FATAL, "sandbox submode is not supported on this platform"); #endif - #ifdef ENABLE_GCOV - if (args.sandbox) error (FATAL, "sandbox submode does not work if gcov is instrumented"); #endif + args.sandbox = true; + } - Option.sorted = SO_UNSORTED; - setMainLoop (interactiveLoop, &args); - setErrorPrinter (jsonErrorPrinter, NULL); - setTagWriter (WRITER_JSON, NULL); - enablePtag (PTAG_JSON_OUTPUT_VERSION, true); + if (Option.interactive & INTERACTIVE_ONESHOT) + { + Assert (args.fname); + loop = interactiveOneshot; + } - json_set_alloc_funcs (eMalloc, eFree); + Option.sorted = SO_UNSORTED; + setJsonMode (true); + setMainLoop (loop, &args); } #endif @@ -2603,7 +2653,7 @@ static void processOutputFormat (const char *const option CTAGS_ATTR_UNUSED, break; #ifdef HAVE_JANSSON case WRITER_JSON: - setJsonMode (); + setJsonMode (false); break; #endif case WRITER_CUSTOM: @@ -2683,17 +2733,57 @@ static void processPseudoTags (const char *const option CTAGS_ATTR_UNUSED, vStringDelete (str); } +static bool inSandbox (void) +{ + return (Option.interactive & INTERACTIVE_WITH_SANDBOX); +} + +static bool inOneshotMode (void) +{ + return (Option.interactive & INTERACTIVE_ONESHOT); +} + +static void oneshotSetLimit (size_t limit) +{ + verbose ("adjust input limit of oneshot mode: %lu", (unsigned long)limit); + oneshotLimit = limit; +} + +static void processOneshotLimit ( + const char *const option, const char *const parameter) +{ + if (parameter == NULL || parameter[0] == '\0') + error (FATAL, "A positive number or 0 is needed after --%s option", option); + + unsigned long limit = 0; + if (!strToULong(parameter, 0, &limit)) + error (FATAL, "Invalid oneshot limit: %s", parameter); + if (limit > SIZE_MAX) + error (FATAL, "Too large limit: %s (> %lu)", + parameter, (unsigned long)SIZE_MAX); + + oneshotSetLimit ((size_t)limit); +} + static void processSortOption ( const char *const option, const char *const parameter) { if (isFalse (parameter)) Option.sorted = SO_UNSORTED; else if (isTrue (parameter)) + { + if (inSandbox () && !inOneshotMode ()) + error (FATAL, "cannot sort in sandbox"); Option.sorted = SO_SORTED; + } else if (strcasecmp (parameter, "f") == 0 || strcasecmp (parameter, "fold") == 0 || strcasecmp (parameter, "foldcase") == 0) + { + if (inSandbox () && !inOneshotMode ()) + error (FATAL, "cannot sort in sandbox"); Option.sorted = SO_FOLDSORTED; + } else error (FATAL, "Invalid value for \"%s\" option", option); } @@ -3077,6 +3167,8 @@ static parametricOption ParametricOptions [] = { { "list-roles", processListRolesOption, true, STAGE_ANY }, { "list-subparsers", processListSubparsersOption, true, STAGE_ANY }, { "maxdepth", processMaxRecursionDepthOption, true, STAGE_ANY }, + { "oneshot", processOneshot, true, STAGE_ANY }, + { "oneshot-limit", processOneshotLimit, true, STAGE_ANY }, { "optlib-dir", processOptlibDir, false, STAGE_ANY }, { "options", processOptionFile, false, STAGE_ANY }, { "options-maybe", processOptionFileMaybe, false, STAGE_ANY }, @@ -4169,11 +4261,6 @@ static void processDumpPreludeOption (const char *const option CTAGS_ATTR_UNUSED exit (0); } -extern bool inSandbox (void) -{ - return (Option.interactive == INTERACTIVE_SANDBOX); -} - extern bool canUseLineNumberAsLocator (void) { return (Option.locate != EX_PATTERN); @@ -4183,7 +4270,7 @@ extern bool isDestinationStdout (void) { bool toStdout = false; - if (Option.filter || Option.interactive || + if (Option.filter || (Option.interactive & INTERACTIVE_MODE) || (Option.tagFileName != NULL && (strcmp (Option.tagFileName, "-") == 0 || strcmp (Option.tagFileName, "/dev/stdout") == 0 ))) diff --git a/main/options.h b/main/options.h index 4726de2b75..045de4fd3c 100644 --- a/main/options.h +++ b/main/options.h @@ -34,9 +34,6 @@ extern void verbose (const char *const format, ...) CTAGS_ATTR_PRINTF (1, 2); #define BEGIN_VERBOSE_IF(COND,VFP) do { if (ctags_verbose || (COND)) { \ FILE* VFP = stderr - -extern bool inSandbox (void); - /* This is for emitting a tag for a common block of Fortran parser*/ extern bool canUseLineNumberAsLocator (void); diff --git a/main/options_p.h b/main/options_p.h index b88b5b3083..1aef8f50a0 100644 --- a/main/options_p.h +++ b/main/options_p.h @@ -105,9 +105,11 @@ typedef struct sOptionValues { bool putFieldPrefix; /* --put-field-prefix */ unsigned int maxRecursionDepth; /* --maxdepth= */ bool fieldsReset; /* --fields=[^+-] */ - enum interactiveMode { INTERACTIVE_NONE = 0, - INTERACTIVE_DEFAULT, - INTERACTIVE_SANDBOX, } interactive; /* --interactive */ + enum interactiveMode { INTERACTIVE_NONE = 0, + INTERACTIVE_MODE = 1 << 0, + INTERACTIVE_WITH_SANDBOX = 1 << 1, + INTERACTIVE_ONESHOT = 1 << 2, + } interactive; /* --interactive */ #ifdef _WIN32 enum filenameSepOp { FILENAME_SEP_NO_REPLACE = false, FILENAME_SEP_USE_SLASH = true, diff --git a/main/parse.c b/main/parse.c index 97444c7313..789b56c784 100644 --- a/main/parse.c +++ b/main/parse.c @@ -4811,7 +4811,7 @@ extern bool parseFileWithMio (const char *const fileName, MIO *mio, { Assert(isLanguageEnabled (language)); - if (Option.filter && ! Option.interactive) + if (Option.filter && ! (Option.interactive & INTERACTIVE_MODE)) openTagFile (); #ifdef HAVE_ICONV @@ -4819,8 +4819,8 @@ extern bool parseFileWithMio (const char *const fileName, MIO *mio, openConverter (getLanguageEncoding (language), Option.outputEncoding); #endif tagFileResized = parseMio (fileName, language, req.mio, req.mtime, true, clientData); - if (Option.filter && ! Option.interactive) - closeTagFile (tagFileResized); + if (Option.filter && ! (Option.interactive & INTERACTIVE_MODE)) + closeTagFile (tagFileResized, false); addTotals (1, 0L, 0L); #ifdef HAVE_ICONV diff --git a/main/seccomp.c b/main/seccomp.c index d60133d451..57afacd5c9 100644 --- a/main/seccomp.c +++ b/main/seccomp.c @@ -17,15 +17,28 @@ #include -int installSyscallFilter (void) +static void installSyscallOpenFilter(scmp_filter_ctx ctx) { - // Use SCMP_ACT_TRAP to get a core dump. - scmp_filter_ctx ctx = seccomp_init (SCMP_ACT_KILL); - if (ctx == NULL) - { - return 1; - } + seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (open), 0); + seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (openat), 0); + verbose ("open/openat "); +} + +static void installSyscallCloseFilter(scmp_filter_ctx ctx) +{ + seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (close), 0); + verbose ("close "); +} + +static void installSyscallCtrlsetFilter(scmp_filter_ctx ctx) +{ + seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (prctl), 0); + seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (seccomp), 0); + verbose ("ctrlset "); +} +static void installSyscallCoresetFilter(scmp_filter_ctx ctx) +{ // Memory allocation. seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (mmap), 0); seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (munmap), 0); @@ -61,7 +74,29 @@ int installSyscallFilter (void) // libxml2 uses pthread_once, which in turn uses a futex seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS (futex), 0); - verbose ("Entering sandbox\n"); + verbose ("coreset "); +} + +int installSyscallFilter (unsigned int set) +{ + // Use SCMP_ACT_TRAP to get a core dump. + scmp_filter_ctx ctx = seccomp_init (SCMP_ACT_KILL); + if (ctx == NULL) + { + return 1; + } + + verbose ("Entering sandbox ("); + if (set & syscall_coreset) + installSyscallCoresetFilter (ctx); + if (set & syscall_open) + installSyscallOpenFilter (ctx); + if (set & syscall_close) + installSyscallCloseFilter (ctx); + if (set & syscall_ctrlset) + installSyscallCtrlsetFilter (ctx); + verbose (")\n"); + int err = seccomp_load (ctx); if (err < 0) { @@ -81,7 +116,7 @@ int installSyscallFilter (void) */ #else -int installSyscallFilter (void) +int installSyscallFilter (unsigned int set) { AssertNotReached (); return -1; diff --git a/main/sort.c b/main/sort.c index 09c6c5fdc6..5c326f4634 100644 --- a/main/sort.c +++ b/main/sort.c @@ -132,7 +132,12 @@ extern void externalSortTags (const bool toStdout, MIO *tagFile) fdsave = dup (fdstdin); if (fdsave < 0) error (FATAL | PERROR, "cannot save stdin fd"); - if (dup2 (fileno (mio_file_get_fp (tagFile)), fdstdin) < 0) + + FILE *tagFP = mio_file_get_fp (tagFile); + if (!tagFP) + error (FATAL, "cannot get FILE object from mio: %p", tagFile); + + if (dup2 (fileno (tagFP), fdstdin) < 0) error (FATAL | PERROR, "cannot redirect stdin"); if (lseek (fdstdin, 0, SEEK_SET) != 0) error (FATAL | PERROR, "cannot rewind tag file"); @@ -169,7 +174,7 @@ extern void externalSortTags (const bool toStdout, MIO *tagFile) vStringDelete (cmd); } -#else +#endif /* * These functions provide a basic internal sort. No great memory @@ -299,5 +304,3 @@ extern void internalSortTags (const bool toStdout, MIO* mio, size_t numTags) free (table [i]); free (table); } - -#endif diff --git a/main/sort_p.h b/main/sort_p.h index 4172ad03d1..7dc6fcf614 100644 --- a/main/sort_p.h +++ b/main/sort_p.h @@ -25,11 +25,11 @@ extern void catFile (MIO *mio); #ifdef EXTERNAL_SORT extern void externalSortTags (const bool toStdout, MIO *tagFile); -#else +#endif + extern void internalSortTags (const bool toStdout, MIO *mio, size_t numTags); -#endif /* mio is closed in this function. */ extern void failedSort (MIO *const mio, const char* msg); diff --git a/main/writer-json.c b/main/writer-json.c index fd827a170c..e3062d12f3 100644 --- a/main/writer-json.c +++ b/main/writer-json.c @@ -54,8 +54,11 @@ static int writeJsonPtagEntry (tagWriter *writer CTAGS_ATTR_UNUSED, const char *const parserName, void *clientData); +static void initJsonWriter (void); + tagWriter jsonWriter = { .oformat = "json", + .init = initJsonWriter, .writeEntry = writeJsonEntry, .writePtagEntry = writeJsonPtagEntry, .printPtagByDefault = true, @@ -67,6 +70,21 @@ tagWriter jsonWriter = { .defaultFileName = NULL, }; +static void initJsonWriter (void) +{ + json_set_alloc_funcs (eMalloc, eFree); + +#ifdef HAVE_SECCOMP + /* The sandbox feature requires this step. */ + /* As of jansson 2.6, the object hashing is seeded off + of /dev/urandom, so trigger the hash seeding + before installing the syscall filter. + */ + json_t * tmp = json_object (); + json_decref (tmp); +#endif +} + static const char* escapeFieldValueRaw (const tagEntryInfo * tag, fieldType ftype, int fieldIndex) { const char *v; diff --git a/main/writer.c b/main/writer.c index defbae55ec..d480158dfd 100644 --- a/main/writer.c +++ b/main/writer.c @@ -45,6 +45,9 @@ extern void setTagWriter (writerType wtype, tagWriter *customWriter) else writer = customWriter; writer->type = wtype; + + if (writer->init) + writer->init (); } extern void writerSetup (MIO *mio, void *clientData) diff --git a/main/writer_p.h b/main/writer_p.h index c7cc363fed..4ba99860db 100644 --- a/main/writer_p.h +++ b/main/writer_p.h @@ -39,6 +39,7 @@ typedef struct sTagWriter tagWriter; struct sTagWriter { const char *oformat; /* name used in CLI: --output-format= * NULL is acceptable.*/ + void (* init) (void); /* Called in setTagWriter() */ int (* writeEntry) (tagWriter *writer, MIO * mio, const tagEntryInfo *const tag, void *clientData); int (* writePtagEntry) (tagWriter *writer, MIO * mio, const ptagDesc *desc, diff --git a/man/ctags.1.rst.in b/man/ctags.1.rst.in index a7712de163..1ee9a2367f 100644 --- a/man/ctags.1.rst.in +++ b/man/ctags.1.rst.in @@ -221,6 +221,48 @@ Input/Output File Options This option is quite esoteric and is empty by default. +``--oneshot=`` + Makes @CTAGS_NAME_EXECUTABLE@ behave as a filter, reading source + file contents from standard input and printing their tags to + standard output. When this option is enabled, the options ``-L``, + ``-f``, ``-o``, and ``--totals`` are ignored. + + @CTAGS_NAME_EXECUTABLE@ assumes ** is the input file + name of the contents. It affects language selection. + ** appears as the input file name in the tags output. + + This option is useful for extracting interesting names in command + output. + + For example, you can extract C preprocessor macro names in the source + code specified by a URL: + + .. code-block:: console + + $ curl -s -o - \ + 'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \ + | ./ctags --oneshot=buf.c --kinds-C='{macro}' + BH_ENTRY buf.c /^#define BH_ENTRY(/;" d file: + ... + + Make the output in JSON format: + + .. code-block:: console + + $ curl -s -o - \ + 'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \ + | ./ctags --oneshot=buf.c --kinds-C='{macro}' --output-format=json \ + | jq . + { + "_type": "tag", + "name": "BH_ENTRY", + "path": "buf.c", + "pattern": "/^#define BH_ENTRY(/", + "file": true, + "kind": "macro" + } + ... + ``--links[=(yes|no)]`` Indicates whether symbolic links (if supported) should be followed. When disabled, symbolic links are ignored. This option is on by default. diff --git a/win32/GNUmakefile b/win32/GNUmakefile index e7de195b5c..4d4936a512 100644 --- a/win32/GNUmakefile +++ b/win32/GNUmakefile @@ -26,11 +26,11 @@ include $(SOURCE_MAK) # exclude some files for Win32 and replace a slash (/) to a backslash (\) MVC_SRCS = $(MVC_GNULIB_SRCS) $(CMDLINE_SRCS) $(LIB_SRCS) $(OPTLIB2C_SRCS) $(PARSER_SRCS) $(OPTSCRIPT_DSL_SRCS) $(DEBUG_SRCS) $(WIN32_SRCS) -MVC_SRCS_EXCLUDE = main/mbcs.c main/seccomp.c main/trace.c +MVC_SRCS_EXCLUDE = main/mbcs.c main/trace.c MVC_SRCS_CONV = $(sort $(subst /,\\,$(filter-out $(MVC_SRCS_EXCLUDE),$(MVC_SRCS)))) MVC_HEADS = $(MVC_GNULIB_HEADS) $(CMDLINE_HEADS) $(LIB_HEADS) $(OPTLIB2C_HEADS) $(PARSER_HEADS) $(OPTSCRIPT_DSL_HEADS) $(DEBUG_HEADS) $(WIN32_HEADS) -MVC_HEADS_EXCLUDE = main/interactive_p.h main/mbcs.h main/mbcs_p.h main/trace.h +MVC_HEADS_EXCLUDE = main/mbcs.h main/mbcs_p.h main/trace.h MVC_HEADS_CONV = $(sort $(subst /,\\,$(filter-out $(MVC_HEADS_EXCLUDE),$(MVC_HEADS)))) MVC_INC_DIRS = ..;../main;../gnulib;../parsers;../parsers/cxx;../dsl; diff --git a/win32/ctags_vs2013.vcxproj b/win32/ctags_vs2013.vcxproj index 334577c97c..d55708db82 100644 --- a/win32/ctags_vs2013.vcxproj +++ b/win32/ctags_vs2013.vcxproj @@ -220,6 +220,7 @@ + @@ -421,6 +422,7 @@ + diff --git a/win32/ctags_vs2013.vcxproj.filters b/win32/ctags_vs2013.vcxproj.filters index cfc511288f..310b6aeb53 100644 --- a/win32/ctags_vs2013.vcxproj.filters +++ b/win32/ctags_vs2013.vcxproj.filters @@ -183,6 +183,9 @@ Source Files\main + + Source Files\main + Source Files\main @@ -782,6 +785,9 @@ Header Files + + Header Files + Header Files