From ba2236facb0fc33e8b5bfa591ae3065400a5c5be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano?= <fmang@mg0.fr>
Date: Sat, 24 Oct 2020 12:58:01 +0200
Subject: [PATCH] Cancel --edit when the editor closes without saving

---
 src/cli.cc       | 35 ++++++++++++++++++++++-------------
 src/opustags.h   |  8 ++++++++
 src/system.cc    |  9 +++++++++
 t/CMakeLists.txt |  1 -
 t/emptier        |  6 ------
 t/opustags.t     |  6 ++----
 6 files changed, 41 insertions(+), 24 deletions(-)
 delete mode 100755 t/emptier

diff --git a/src/cli.cc b/src/cli.cc
index 0e2ffa1..0c91263 100644
--- a/src/cli.cc
+++ b/src/cli.cc
@@ -283,25 +283,39 @@ static ot::status edit_tags_interactively(ot::opus_tags& tags, const std::option
 		return {ot::st::error,
 		        "No editor specified in environment variable VISUAL or EDITOR."};
 
+	// Building the temporary tags file.
 	std::string tags_path = base_path.value_or("tags") + ".XXXXXX.opustags";
 	int fd = mkstemps(const_cast<char*>(tags_path.data()), 9);
 	FILE* tags_file;
 	if (fd == -1 || (tags_file = fdopen(fd, "w")) == nullptr)
 		return {ot::st::standard_error,
 		        "Could not open '" + tags_path + "': " + strerror(errno)};
-
 	ot::print_comments(tags.comments, tags_file);
-	fputs("\n"
-	      "# Edit these tags to your liking and close your editor to apply them.\n"
-	      "# If you delete all the tags however, tag edition will be cancelled.\n",
-	      tags_file);
 	if (fclose(tags_file) != 0)
-		return {ot::st::standard_error, "fclose error: "s + strerror(errno)};
+		return {ot::st::standard_error, tags_path + ": fclose error: "s + strerror(errno)};
 
-	ot::status rc = ot::run_editor(editor, tags_path.c_str());
-	if (rc != ot::st::ok)
+	// Spawn the editor, and watch the modification timestamps.
+	ot::status rc;
+	timespec before, after;
+	if ((rc = ot::get_file_timestamp(tags_path.c_str(), before)) != ot::st::ok)
 		return rc;
+	ot::status editor_rc = ot::run_editor(editor, tags_path.c_str());
+	if ((rc = ot::get_file_timestamp(tags_path.c_str(), after)) != ot::st::ok)
+		return rc; // probably because the file was deleted
+	bool modified = (before.tv_sec != after.tv_sec || before.tv_nsec != after.tv_nsec);
+	if (editor_rc != ot::st::ok) {
+		if (modified)
+			fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str());
+		else
+			remove(tags_path.c_str());
+		return rc;
+	} else if (!modified) {
+		remove(tags_path.c_str());
+		fputs("Cancelling edition because the tags file was not modified.\n", stderr);
+		return ot::st::cancel;
+	}
 
+	// Applying the new tags.
 	tags_file = fopen(tags_path.c_str(), "r");
 	if (tags_file == nullptr)
 		return {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)};
@@ -311,11 +325,6 @@ static ot::status edit_tags_interactively(ot::opus_tags& tags, const std::option
 	}
 	fclose(tags_file);
 
-	if (tags.comments.size() == 0) {
-		remove(tags_path.c_str()); // it’s empty anyway
-		return {ot::st::error, "Tag edition was cancelled because all the tags were deleted."};
-	}
-
 	// Remove the temporary tags file only on success, because unlike the
 	// partial Ogg file that is irrecoverable, the edited tags file
 	// contains user data, so let’s leave users a chance to recover it.
diff --git a/src/opustags.h b/src/opustags.h
index fa9fc5d..82f80c0 100644
--- a/src/opustags.h
+++ b/src/opustags.h
@@ -27,6 +27,7 @@
 #include <iconv.h>
 #include <ogg/ogg.h>
 #include <stdio.h>
+#include <time.h>
 
 #include <functional>
 #include <list>
@@ -58,6 +59,7 @@ enum class st {
 	error,
 	standard_error, /**< Error raised by the C standard library. */
 	int_overflow,
+	cancel,
 	/* System */
 	badly_encoded,
 	information_lost,
@@ -172,6 +174,12 @@ private:
  */
 ot::status run_editor(const char* editor, const char* path);
 
+/**
+ * Return the specified path’s mtime, i.e. the last data modification
+ * timestamp.
+ */
+ot::status get_file_timestamp(const char* path, timespec& mtime);
+
 /** \} */
 
 /***********************************************************************************************//**
diff --git a/src/system.cc b/src/system.cc
index 145a893..a6f3fd4 100644
--- a/src/system.cc
+++ b/src/system.cc
@@ -177,3 +177,12 @@ ot::status ot::run_editor(const char* editor, const char* path)
 
 	return st::ok;
 }
+
+ot::status ot::get_file_timestamp(const char* path, timespec& mtime)
+{
+	struct stat st;
+	if (stat(path, &st) == -1)
+		return {st::standard_error, path + ": stat error: "s + strerror(errno)};
+	mtime = st.st_mtim; // more precise than st_mtime
+	return st::ok;
+}
diff --git a/t/CMakeLists.txt b/t/CMakeLists.txt
index f1fafa2..42d6703 100644
--- a/t/CMakeLists.txt
+++ b/t/CMakeLists.txt
@@ -14,7 +14,6 @@ add_executable(oggdump EXCLUDE_FROM_ALL oggdump.cc)
 target_link_libraries(oggdump ot)
 
 configure_file(gobble.opus . COPYONLY)
-configure_file(emptier . COPYONLY)
 
 add_custom_target(
 	check
diff --git a/t/emptier b/t/emptier
deleted file mode 100755
index f463bfd..0000000
--- a/t/emptier
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-cat > "$1" << EOF
-
-# Here’s a file with nothing but comments and newlines.
-
-EOF
diff --git a/t/opustags.t b/t/opustags.t
index e81f159..9308ef1 100755
--- a/t/opustags.t
+++ b/t/opustags.t
@@ -230,14 +230,12 @@ $ENV{EDITOR} = 'sed -i -e y/a/A/';
 is_deeply(opustags(qw(gobble.opus --add artist=aaah -o screaming.opus -e)), ['', '', 0], 'edit a file with EDITOR');
 is(md5('screaming.opus'), '682229df1df6b0ca147e2778737d449e', 'the tags were modified');
 
-$ENV{EDITOR} = './emptier';
-is_deeply(opustags(qw(--add mystery=1 -i screaming.opus -e)), ['', "screaming.opus: error: Tag edition was cancelled because all the tags were deleted.\n", 256], 'edit a file with EDITOR');
+$ENV{EDITOR} = 'true';
+is_deeply(opustags(qw(--add mystery=1 -i screaming.opus -e)), ['', "Cancelling edition because the tags file was not modified.\n", 256], 'close -e without saving');
 is(md5('screaming.opus'), '682229df1df6b0ca147e2778737d449e', 'the tags were not modified');
 
-$ENV{EDITOR} = '';
 unlink('screaming.opus');
 
-
 ####################################################################################################
 # Test muxed streams