Add <try>/<catch> tag, fix a real infinite-loop bug it exposed in Tag's
split-container mechanism
<try>...container...<catch>...container2...</catch-less, matching <else>'s
own convention> intercepts any error that would otherwise propagate up and
halt the whole script - a coreutils::Exception (what every other tag
actually throws), any other std::exception (e.g. the
std::filesystem::filesystem_error <whiledir> can raise), or anything else.
Optional name=/scope= keywords bind the caught error's message to a
variable for the catch block to reference, matching <for>'s own
name=/scope= convention. New class named __trytag rather than __try: <ios>
transitively defines a preprocessor macro "#define __try try" (an SEH-
compatibility shim pulled in on this toolchain) that silently rewrites the
identifier __try to the keyword try wherever it appears, breaking
compilation - confirmed via `g++ -E -dM` before picking this name. The JET
tag name itself is still plain <try>.
Reuses the same container/container2 split mechanism <if>/<else> already
has (Tag's constructor takes an optional splitTagName), which surfaced a
real, previously-latent bug: the code that consumes the split tag's own
opening delimiter after a match was hardcoded to the literal string
"<else>" instead of being built from splitTagName. This happened to work
for every existing split-tag user (<if>, <while>, <until> all use "else"),
but for <try>'s "catch" the hardcoded ifNext("<else>") never matched, so
the cursor never advanced past <catch> and the outer scan loop matched the
same unconsumed <catch> forever - a genuine infinite loop, not just wrong
output, confirmed via a real hung jet-2.0 run before diagnosis. Fixed by
building the actual expected delimiter ("<" + splitTagName + ">")
dynamically instead.
Verified via real jet-2.0 script execution (not just compilation): a
caught <read> failure correctly binds its error text to a name= variable
and skips straight to the catch block, discarding whatever partial output
the failed attempt had already written; an untriggered <try> runs its own
content normally and skips <catch> entirely; a <try> with no <catch> at
all silently swallows the error and the script continues past it. Also
regression-tested <if>/<else> and <while> (both depend on the exact same
fixed code path) to confirm nothing broke for the existing "else" case.
This commit is contained in:
1 parent
bc3c50f045
commit
76ea89e79c
3 files changed
+107
-2
No files matched your search
@@ -25,6 +25,7 @@
|
||||
#include "__dotag.h"
|
||||
#include "__stream.h"
|
||||
#include "__dump.h"
|
||||
#include "__trytag.h"
|
||||
#include "Operand.h"
|
||||
#include <iostream>
|
||||
|
||||
@@ -90,8 +91,22 @@ namespace jet {
|
||||
} else if(hasSplitTag && ifSplitTagName(in)) {
|
||||
hasContainer2 = true;
|
||||
container = coreutils::ZString(start, end - start);
|
||||
in.ifNext("<else>");
|
||||
start = in.getCursor();
|
||||
// ifSplitTagName() is a pure lookahead (push()/pop()'s the
|
||||
// cursor back regardless of match - see its own body), so
|
||||
// the actual split-tag opening delimiter still has to be
|
||||
// consumed here. This used to be hardcoded to the literal
|
||||
// "<else>" - fine for <if>/<while>/<until> (which all use
|
||||
// splitTagName "else"), but wrong for any other split tag
|
||||
// name (e.g. <try>'s "catch") - a hardcoded literal that
|
||||
// never matches means this ifNext() silently fails, the
|
||||
// cursor never advances past <catch>, and the outer loop
|
||||
// matches the same unconsumed <catch> on every subsequent
|
||||
// iteration forever. Build the real expected delimiter
|
||||
// from splitTagName itself so it works for any split tag.
|
||||
coreutils::MString splitOpenTag;
|
||||
splitOpenTag << "<" << splitTagName << ">";
|
||||
in.ifNext(splitOpenTag);
|
||||
start = in.getCursor();
|
||||
} else if(ifNested(in)) {
|
||||
} else
|
||||
in.nextChar();
|
||||
@@ -211,6 +226,9 @@ namespace jet {
|
||||
} else if(ifTagName(in, "until")) {
|
||||
__until _until(in, out, global, this, local);
|
||||
continue;
|
||||
} else if(ifTagName(in, "try")) {
|
||||
__trytag _trytag(in, out, global, this, local);
|
||||
continue;
|
||||
} else if(ifTagName(in, "header")) {
|
||||
__header _header(in, out, global, this, local);
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "__trytag.h"
|
||||
#include "Exception.h"
|
||||
#include <exception>
|
||||
|
||||
namespace jet {
|
||||
|
||||
__trytag::__trytag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local)
|
||||
: Tag(in, parentOut, global, parent, this, "catch") {
|
||||
|
||||
coreutils::MString errorText;
|
||||
bool failed = false;
|
||||
|
||||
try {
|
||||
processContainer(container);
|
||||
} catch(coreutils::Exception &e) {
|
||||
errorText = e.text;
|
||||
failed = true;
|
||||
} catch(std::exception &e) {
|
||||
errorText = e.what();
|
||||
failed = true;
|
||||
} catch(...) {
|
||||
errorText = "unknown error";
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if(failed) {
|
||||
// Discard whatever the failed attempt had already written before
|
||||
// it threw - out is this tag's own accumulating output buffer
|
||||
// (processContainer(container) above wrote into it directly), so
|
||||
// without this a caught mid-render failure would leave partial
|
||||
// try-block output mixed in ahead of the catch block's own.
|
||||
out = "";
|
||||
if(keywordDefined("name"))
|
||||
storeVariable(resolveKeyword("name"), errorText,
|
||||
keywordDefined("scope") ? resolveKeyword("scope") : "global");
|
||||
if(hasContainer2)
|
||||
processContainer(container2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#ifndef ____trytag_h__
|
||||
#define ____trytag_h__
|
||||
|
||||
#include "Tag.h"
|
||||
#include "ZString.h"
|
||||
#include "MString.h"
|
||||
|
||||
namespace jet {
|
||||
|
||||
///
|
||||
/// <try>...container...<catch>...container2...</catch></try>
|
||||
///
|
||||
/// Runs its own container the same as any other tag; if that raises an
|
||||
/// error that would otherwise propagate up and halt the whole script -
|
||||
/// a coreutils::Exception (what every other tag actually throws), any
|
||||
/// other std::exception (e.g. the std::filesystem::filesystem_error
|
||||
/// <whiledir> can raise), or anything else - the <catch> container (if
|
||||
/// present) runs instead, with any partial output the failed attempt
|
||||
/// had already written discarded first so the result is cleanly either
|
||||
/// the try's output or the catch's, never a mix of both.
|
||||
///
|
||||
/// Optional keywords, both meaningful only on catch: name (a variable
|
||||
/// to receive the caught error's message text) and scope (global by
|
||||
/// default, or local/parent - same convention as <for>'s own name/scope
|
||||
/// pair).
|
||||
///
|
||||
/// Named __trytag rather than __try: <ios> transitively defines a
|
||||
/// preprocessor macro "#define __try try" (an SEH-compatibility shim
|
||||
/// pulled in on this toolchain) that silently rewrites the identifier
|
||||
/// __try to the literal keyword try wherever it appears, breaking
|
||||
/// compilation - confirmed via `g++ -E -dM` before picking this name,
|
||||
/// not guessed. The JET tag name itself is still plain <try> - this
|
||||
/// only affects the internal C++ class name, matched by ifTagName(in,
|
||||
/// "try") in Tag.cpp regardless of what the class is called.
|
||||
///
|
||||
|
||||
class __trytag : public Tag {
|
||||
|
||||
public:
|
||||
__trytag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user