commit a0b09f463d70de26af8e0c0d3fb643fe971c24b0
Author: Pavel Sanda <[email protected]>
Date:   Wed May 27 13:01:14 2026 +0200

    Implement startscript with cleaner argument handling.
    
    New startscriptArgv added, the external calls via runCommand() with
    user-crafted input would be better to route via this gate and avoid
    parsecmd/splitCommand parsers altogether. Completion handling shared with
    original startscript.
    
    The argv form is wait-only and does not support TEXINPUTS env
    prefix or shell-style redirects (no current caller needs them). If
    a future caller does, extend the API rather than reintroducing the
    string round-trip.
    
    Tier 01 DiD / argv 01: argv-form API; re-closes 00a/00b
                           structurally, removes popen runCommand
    
    https://www.mail-archive.com/[email protected]/msg226742.html
    
    Assisted-by: Claude Opus 4.7
---
 src/support/Systemcall.cpp      | 199 ++++++++++++++++++++++++++++++++--------
 src/support/Systemcall.h        |  44 +++++++++
 src/support/SystemcallPrivate.h |  17 ++++
 3 files changed, 221 insertions(+), 39 deletions(-)

diff --git a/src/support/Systemcall.cpp b/src/support/Systemcall.cpp
index ab3fd2ed50..a9bdde41e3 100644
--- a/src/support/Systemcall.cpp
+++ b/src/support/Systemcall.cpp
@@ -253,6 +253,49 @@ void Systemcall::killscript()
 }
 
 
+namespace {
+
+// Shared tail of startscript() and startscriptArgv(): wait for the
+// process spawned via d.startProcess() to start, run, and finish, then
+// translate state/exit-code into a Systemcall::ReturnValue. Body is
+// the verbatim post-startProcess block extracted from startscript().
+int waitForCompletion(SystemcallPrivate & d, QString const & cmd,
+                      bool do_events, int timeout_ms)
+{
+       if (d.state == SystemcallPrivate::Error
+                       || !d.waitWhile(SystemcallPrivate::Starting, do_events, 
-1)) {
+               if (d.state == SystemcallPrivate::Error) {
+                       LYXERR0("Systemcall: '" << cmd << "' did not start!");
+                       LYXERR0("error " << d.errorMessage());
+                       return Systemcall::NOSTART;
+               } else if (d.state == SystemcallPrivate::Killed) {
+                       LYXERR0("Killed: " << cmd);
+                       return Systemcall::KILLED;
+               }
+       }
+
+       if (!d.waitWhile(SystemcallPrivate::Running, do_events, timeout_ms)) {
+               if (d.state == SystemcallPrivate::Killed) {
+                       LYXERR0("Killed: " << cmd);
+                       return Systemcall::KILLED;
+               }
+               LYXERR0("Systemcall: '" << cmd << "' did not finish!");
+               LYXERR0("error " << d.errorMessage());
+               LYXERR0("status " << d.exitStatusMessage());
+               return Systemcall::TIMEOUT;
+       }
+
+       int const exit_code = d.exitCode();
+       if (exit_code) {
+               LYXERR0("Systemcall: '" << cmd << "' finished with exit code " 
<< exit_code);
+       }
+
+       return exit_code;
+}
+
+} // namespace
+
+
 int Systemcall::startscript(Starttype how, string const & what,
                            string const & path, string const & lpath,
                            bool process_events)
@@ -276,36 +319,49 @@ int Systemcall::startscript(Starttype how, string const & 
what,
        if (how == DontWait && d.state == SystemcallPrivate::Running)
                return OK;
 
-       if (d.state == SystemcallPrivate::Error
-                       || !d.waitWhile(SystemcallPrivate::Starting, do_events, 
-1)) {
-               if (d.state == SystemcallPrivate::Error) {
-                       LYXERR0("Systemcall: '" << cmd << "' did not start!");
-                       LYXERR0("error " << d.errorMessage());
-                       return NOSTART;
-               } else if (d.state == SystemcallPrivate::Killed) {
-                       LYXERR0("Killed: " << cmd);
-                       return KILLED;
-               }
-       }
+       return waitForCompletion(d, cmd, do_events, os::timeout_ms());
+}
 
-       if (!d.waitWhile(SystemcallPrivate::Running, do_events,
-                        os::timeout_ms())) {
-               if (d.state == SystemcallPrivate::Killed) {
-                       LYXERR0("Killed: " << cmd);
-                       return KILLED;
-               }
-               LYXERR0("Systemcall: '" << cmd << "' did not finish!");
-               LYXERR0("error " << d.errorMessage());
-               LYXERR0("status " << d.exitStatusMessage());
-               return TIMEOUT;
-       }
 
-       int const exit_code = d.exitCode();
-       if (exit_code) {
-               LYXERR0("Systemcall: '" << cmd << "' finished with exit code " 
<< exit_code);
-       }
+int Systemcall::startscriptArgv(string const & program,
+                                vector<string> const & args,
+                                string * stdout_capture)
+{
+       // Build the QStringList that will be handed directly to QProcess.
+       // Each element becomes one argv slot in the child; nothing here
+       // goes through commandPrep / parsecmd / QProcess::splitCommand,
+       // so crafted bytes in \p args cannot be re-split into a different
+       // argv than the caller wrote. That is the security property of
+       // this entry point.
+       QString const qprogram = QString::fromLocal8Bit(program.c_str());
+       QStringList qargs;
+       qargs.reserve(int(args.size()));
+       for (string const & a : args)
+               qargs << QString::fromLocal8Bit(a.c_str());
+
+       // Display string for log / cancel-dialog. Never executed.
+       QString display = qprogram;
+       for (QString const & a : qargs)
+               display += QLatin1Char(' ') + a;
 
-       return exit_code;
+       if (verbose)
+               lyxerr << "\nRunning: " << fromqstr(display) << endl;
+       else
+               LYXERR(Debug::INFO, "Running: " << fromqstr(display));
+
+       string const empty;
+       SystemcallPrivate d(empty, empty, empty);
+       QByteArray captured;
+       if (stdout_capture)
+               d.setStdoutCapture(&captured);
+
+       d.startProcess(qprogram, qargs, display);
+       int const ret = waitForCompletion(d, display, false, os::timeout_ms());
+
+       if (stdout_capture)
+               stdout_capture->assign(captured.constData(), captured.size());
+
+       return ret;
 }
 
 
@@ -315,7 +371,8 @@ bool SystemcallPrivate::kill_script = false;
 SystemcallPrivate::SystemcallPrivate(std::string const & sf, std::string const 
& of,
                                      std::string const & ef)
        : state(Error), process_(new QProcess), out_index_(0), err_index_(0),
-         in_file_(sf), out_file_(of), err_file_(ef), process_events_(false)
+         in_file_(sf), out_file_(of), err_file_(ef), process_events_(false),
+         capture_stdout_(nullptr)
 {
        if (!in_file_.empty())
                
process_->setStandardInputFile(QString::fromLocal8Bit(in_file_.c_str()));
@@ -432,6 +489,31 @@ void SystemcallPrivate::startProcess(QString const & cmd, 
string const & path,
 }
 
 
+void SystemcallPrivate::startProcess(QString const & program,
+                                     QStringList const & args,
+                                     QString const & display)
+{
+       // cmd_ holds the display string used by the cancel dialog and log
+       // messages only. The actual launch goes through QProcess::start
+       // with (program, args) as separate arguments, bypassing
+       // QProcess::splitCommand entirely - so nothing between this point
+       // and the child's argv vector can reshape it. Detached/DontWait
+       // is not supported for the argv form (the sole caller is
+       // Systemcall::startscriptArgv, which is wait-only).
+       cmd_ = display.isEmpty() ? program : display;
+       if (process_) {
+               state = SystemcallPrivate::Starting;
+               process_->start(program, args);
+       }
+}
+
+
+void SystemcallPrivate::setStdoutCapture(QByteArray * dest)
+{
+       capture_stdout_ = dest;
+}
+
+
 bool SystemcallPrivate::waitAndCheck()
 {
        Sleep::millisec(100);
@@ -551,17 +633,22 @@ SystemcallPrivate::~SystemcallPrivate()
 
 void SystemcallPrivate::stdOut()
 {
-       if (process_) {
-               char c;
-               process_->setReadChannel(QProcess::StandardOutput);
-               while (process_->getChar(&c)) {
-                       out_data_[out_index_++] = c;
-                       if (c == '\n' || out_index_ + 1 == buffer_size_) {
-                               out_data_[out_index_] = '\0';
-                               out_index_ = 0;
-                               
ProgressInterface::instance()->appendMessage(QString::fromLocal8Bit(out_data_));
-                               cout << out_data_;
-                       }
+       if (!process_)
+               return;
+       if (capture_stdout_) {
+               // argv-form callers want raw stdout in a buffer, not echoed.
+               capture_stdout_->append(process_->readAllStandardOutput());
+               return;
+       }
+       char c;
+       process_->setReadChannel(QProcess::StandardOutput);
+       while (process_->getChar(&c)) {
+               out_data_[out_index_++] = c;
+               if (c == '\n' || out_index_ + 1 == buffer_size_) {
+                       out_data_[out_index_] = '\0';
+                       out_index_ = 0;
+                       
ProgressInterface::instance()->appendMessage(QString::fromLocal8Bit(out_data_));
+                       cout << out_data_;
                }
        }
 }
@@ -701,6 +788,40 @@ void SystemcallPrivate::killProcess(QProcess * p)
        }
 }
 
+vector<string> splitCommandTokens(string const & cmd)
+{
+       vector<string> argv;
+       size_t i = 0;
+       while (i < cmd.size()) {
+
+               // skip to the next token
+               while (i < cmd.size() && (cmd[i] == ' ' || cmd[i] == '\t'))
+                       ++i;
+               if (i >= cmd.size())
+                       break;
+
+               string tok;
+
+               // token quoted
+               if (cmd[i] == '"') {
+                       ++i;
+                       while (i < cmd.size() && cmd[i] != '"')
+                               tok += cmd[i++];
+                       if (i < cmd.size())
+                               ++i;    // step over closing quote
+
+               // token unquoted
+               } else {
+                       while (i < cmd.size() && cmd[i] != ' ' && cmd[i] != 
'\t')
+                               tok += cmd[i++];
+               }
+               if (!tok.empty())
+                       argv.push_back(tok);
+       }
+       return argv;
+}
+
+
 } // namespace support
 } // namespace lyx
 
diff --git a/src/support/Systemcall.h b/src/support/Systemcall.h
index d73a928097..2ce3f40153 100644
--- a/src/support/Systemcall.h
+++ b/src/support/Systemcall.h
@@ -15,6 +15,9 @@
 #ifndef SYSTEMCALL_H
 #define SYSTEMCALL_H
 
+#include <string>
+#include <vector>
+
 namespace lyx {
 namespace support {
 
@@ -64,8 +67,49 @@ public:
                        std::string const & path = empty_string(),
                        std::string const & lpath = empty_string(),
                        bool process_events = false);
+
+       /** Argv-form invocation: shell-free, parser-free.
+        *
+        *  Builds an argv vector from (program, args) and hands it directly
+        *  to QProcess::start(program, QStringList), which calls execvp()
+        *  on Unix and CreateProcess() on Windows. Each element of \p args
+        *  reaches the child as one argv slot, byte-for-byte.
+        *
+        *  Note: although string-form startscript() does not invoke a shell
+        *  it resplits into different argv, which is weak from the start.
+        *
+        *  Wait-only (no DontWait/WaitLoop equivalent), no path/lpath
+        *  TEXINPUTS prefix, no shell-style redirects. If a future caller
+        *  needs any of those, extend the API rather than reintroducing the
+        *  string round-trip.
+        *
+        *  \p stdout_capture  if non-null, child stdout is captured here.
+        */
+       int startscriptArgv(std::string const & program,
+                           std::vector<std::string> const & args,
+                           std::string * stdout_capture = nullptr);
 };
 
+
+/// Split a shell-style command string into space-separated tokens, with
+/// double-quoted tokens copied verbatim. Used to migrate string-form
+/// invocations to the argv-form of Systemcall::startscriptArgv without
+/// going through a shell.
+///
+/// Not a full shell parser. Space and tab separate, empty tokens are
+/// dropped, and there is no handling of $VAR / $(), backslash escapes or
+/// single quotes.
+/// A double quote is only a delimiter at the start of a token: inside a
+/// token it is copied as an ordinary character, unlike a shell, which would
+/// remove it. An unterminated quote is not an error - it is dropped and
+/// the remainder of the string becomes a single token.
+///
+/// Used to split only strings LyX produced itself (python) or that come from
+/// user preferences. Document-supplied values must be appended as their own
+/// argv elements, never parsed here.
+std::vector<std::string> splitCommandTokens(std::string const & cmd);
+
+
 } // namespace support
 } // namespace lyx
 
diff --git a/src/support/SystemcallPrivate.h b/src/support/SystemcallPrivate.h
index 19771dfc73..42cb922696 100644
--- a/src/support/SystemcallPrivate.h
+++ b/src/support/SystemcallPrivate.h
@@ -12,8 +12,10 @@
 #ifndef SYSTEMCALLPRIVATE_H
 #define SYSTEMCALLPRIVATE_H
 
+#include <QByteArray>
 #include <QObject>
 #include <QProcess>
+#include <QStringList>
 
 #include <string>
 
@@ -47,6 +49,19 @@ public:
        void startProcess(QString const & cmd, std::string const & path,
                          std::string const & lpath, bool detach);
 
+       /// Argv-form launch: \p program and \p args go directly to
+       /// QProcess::start(program, args), bypassing QProcess::splitCommand
+       /// (which the string-form overload above uses to recover argv from
+       /// the command line). The child sees each element of \p args as
+       /// one argv slot byte-for-byte. \p display is shown in log messages
+       /// and the cancel-dialog text but is never executed. Wait-only.
+       void startProcess(QString const & program, QStringList const & args,
+                         QString const & display);
+
+       /// When non-null, stdOut() routes incoming bytes into *dest
+       /// instead of echoing to the progress UI / std::cout.
+       void setStdoutCapture(QByteArray * dest);
+
        int exitCode();
 
        QString errorMessage() const;
@@ -92,6 +107,8 @@ private:
 
        QString cmd_;
        bool process_events_;
+       /// If non-null, stdOut() appends here instead of echoing.
+       QByteArray * capture_stdout_;
 
        void waitAndProcessEvents();
        void processEvents();
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to