From c2a0726fd41983c3d924eec9c126a6c79fc2a8dd Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:24:50 +0100 Subject: [PATCH 1/2] Keep hype render from overwriting its presentation or a half-written manifest render --slide wrote its PNG to any -o, the presentation included, and rendering a whole deck wrote slides.json in place without checking the write, so a full disk left a truncated manifest behind a successful exit. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Codex --- src/cli.cpp | 3 +++ src/deck.cpp | 8 +++++--- tests/test_cli.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/cli.cpp b/src/cli.cpp index 0c1588a..6648063 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -249,6 +249,9 @@ int render(const QStringList &arguments) { output = name; else if (QFileInfo(output).isDir()) output = QDir(output).filePath(name); + const QString target = QFileInfo(output).canonicalFilePath(); + if (!target.isEmpty() && target == QFileInfo(deck.path()).canonicalFilePath()) + return fail("Choose an output other than the presentation itself."); QDir().mkpath(QFileInfo(output).absolutePath()); // A slide with problems still renders, with the problems on a banner, so it can be looked at. const auto problems = slideProblemsAt(deck, parsed, number - 1); diff --git a/src/deck.cpp b/src/deck.cpp index ce55a64..d6bb773 100644 --- a/src/deck.cpp +++ b/src/deck.cpp @@ -1622,10 +1622,12 @@ bool Deck::renderImages(const QString &directory, int width, bool powerPoint) { } slides.append(entry); } - QFile manifest(directory + "/slides.json"); - if (!manifest.open(QIODevice::WriteOnly)) + QSaveFile manifest(directory + "/slides.json"); + const QByteArray json = QJsonDocument(QJsonObject{{"title", title()}, {"slides", slides}}).toJson(); + if (!manifest.open(QIODevice::WriteOnly) || manifest.write(json) != json.size() || !manifest.commit()) { + setStatus("Could not save slides.json: " + manifest.errorString()); return false; - manifest.write(QJsonDocument(QJsonObject{{"title", title()}, {"slides", slides}}).toJson()); + } return true; } bool Deck::exportPptx(const QString &path) { diff --git a/tests/test_cli.py b/tests/test_cli.py index 95dc815..0f9da19 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -53,6 +53,16 @@ def test_new_starts_a_themed_presentation_without_overwriting(self): self.assertIn('already exists', self.hype('new', self.deck, code=1).stderr) self.assertIn('paper', self.hype('new', self.root / 'other.md', '--theme', 'nope', code=1).stderr) + def test_render_never_overwrites_the_presentation_and_reports_its_manifest(self): + self.write(DECK) + link = self.root / 'link.md' + link.symlink_to(self.deck) + for output in (self.deck, link): + self.assertIn('presentation itself', self.hype('render', self.deck, '--slide', 1, '-o', output, code=1).stderr) + self.assertEqual(self.deck.read_text(), DECK) + (self.root / 'slides/slides.json').mkdir(parents=True) + self.assertIn('slides.json', self.hype('render', self.deck, '-o', self.root / 'slides', code=1).stderr) + def test_check_reports_every_problem_with_slide_and_line(self): self.write(DECK + '\n---\n\n![](missing.png)\n\n---\n\n![bogus=1](gone.png)\n\n---\n') report = json.loads(self.hype('check', self.deck, '--json', code=1).stdout) From fb2ad2affe1d70c5ca473f15cfce59c388aefd58 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:29:18 +0100 Subject: [PATCH 2/2] Write a rendered slide beside its target and rename it over A hard link to the presentation passed the path check and was overwritten in place. Renaming replaces the link instead, and a failed render no longer leaves a half-written PNG. Co-Authored-By: Claude Opus 5.5 --- src/cli.cpp | 5 ++++- tests/test_cli.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli.cpp b/src/cli.cpp index 6648063..e9f3a46 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -257,7 +257,10 @@ int render(const QStringList &arguments) { const auto problems = slideProblemsAt(deck, parsed, number - 1); QString warning; const QImage image = paint(deck, number - 1, width, &warning); - if (!image.save(output, "PNG")) + // Written beside and renamed over, so an existing file, or a hard link to the + // presentation, is replaced rather than overwritten in place. + QSaveFile file(output); + if (!file.open(QIODevice::WriteOnly) || !image.save(&file, "PNG") || !file.commit()) return fail("Could not save " + output); int errors = 0; QJsonArray list; diff --git a/tests/test_cli.py b/tests/test_cli.py index 0f9da19..4c43ac9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -60,6 +60,11 @@ def test_render_never_overwrites_the_presentation_and_reports_its_manifest(self) for output in (self.deck, link): self.assertIn('presentation itself', self.hype('render', self.deck, '--slide', 1, '-o', output, code=1).stderr) self.assertEqual(self.deck.read_text(), DECK) + alias = self.root / 'alias.md' + os.link(self.deck, alias) + self.hype('render', self.deck, '--slide', 1, '-o', alias) + self.assertEqual(self.deck.read_text(), DECK) + self.png_size(alias) (self.root / 'slides/slides.json').mkdir(parents=True) self.assertIn('slides.json', self.hype('render', self.deck, '-o', self.root / 'slides', code=1).stderr)