From 6b1c884eb5b2720b8e4db02ed901bc24784d9ed3 Mon Sep 17 00:00:00 2001
From: David Capello <davidcapello@gmail.com>
Date: Wed, 4 Jul 2018 12:35:15 -0300
Subject: [PATCH] Add code to check integrity of backups

---
 src/app/CMakeLists.txt            |   1 +
 src/app/console.cpp               |   6 +-
 src/app/crash/backup_observer.cpp |  42 ++++++++-
 src/app/crash/session.cpp         |  31 +++++--
 src/app/crash/session.h           |   4 +-
 src/app/document.cpp              |   2 +-
 src/app/document_diff.cpp         | 144 ++++++++++++++++++++++++++++++
 src/app/document_diff.h           |  45 ++++++++++
 src/ui/manager.cpp                |  20 ++++-
 src/ui/message.h                  |  12 +++
 src/ui/message_type.h             |   3 +-
 src/ui/system.cpp                 |  13 +++
 src/ui/system.h                   |   2 +
 src/ui/widget.cpp                 |   4 +
 14 files changed, 315 insertions(+), 14 deletions(-)
 create mode 100644 src/app/document_diff.cpp
 create mode 100644 src/app/document_diff.h

diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index b93aa9bb6..2fc1326c7 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -498,6 +498,7 @@ add_library(app-lib
   context_flags.cpp
   document.cpp
   document_api.cpp
+  document_diff.cpp
   document_exporter.cpp
   document_range.cpp
   document_range_ops.cpp
diff --git a/src/app/console.cpp b/src/app/console.cpp
index 2da2c8ef4..5673e4874 100644
--- a/src/app/console.cpp
+++ b/src/app/console.cpp
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2001-2017  David Capello
+// Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
 // the End-User License Agreement for Aseprite.
@@ -22,6 +22,7 @@
 #include "app/context.h"
 #include "app/modules/gui.h"
 #include "app/ui/status_bar.h"
+#include "ui/system.h"
 
 namespace app {
 
@@ -39,6 +40,9 @@ static bool has_text = false;
 Console::Console(Context* ctx)
   : m_withUI(false)
 {
+  if (!ui::is_ui_thread())
+    return;
+
   if (ctx)
     m_withUI = (ctx->isUIAvailable());
   else
diff --git a/src/app/crash/backup_observer.cpp b/src/app/crash/backup_observer.cpp
index 83063d096..4509ce9e0 100644
--- a/src/app/crash/backup_observer.cpp
+++ b/src/app/crash/backup_observer.cpp
@@ -4,6 +4,13 @@
 // This program is distributed under the terms of
 // the End-User License Agreement for Aseprite.
 
+// Uncomment if you want to test the backup process each 5 secondsh
+//#define TEST_BACKUPS_WITH_A_SHORT_PERIOD
+
+// Uncomment if you want to check that backups are correctly saved
+// after being saved.
+//#define TEST_BACKUP_INTEGRITY
+
 #ifdef HAVE_CONFIG_H
 #include "config.h"
 #endif
@@ -13,6 +20,8 @@
 #include "app/app.h"
 #include "app/crash/session.h"
 #include "app/document.h"
+#include "app/document_access.h"
+#include "app/document_diff.h"
 #include "app/pref/preferences.h"
 #include "base/bind.h"
 #include "base/chrono.h"
@@ -20,6 +29,10 @@
 #include "base/scoped_lock.h"
 #include "doc/context.h"
 
+#ifdef TEST_BACKUP_INTEGRITY
+#include "ui/system.h"
+#endif
+
 namespace app {
 namespace crash {
 
@@ -84,7 +97,7 @@ void BackupObserver::backgroundThread()
 {
   int normalPeriod = int(60.0*Preferences::instance().general.dataRecoveryPeriod());
   int lockedPeriod = 5;
-#if 0                           // Just for testing purposes
+#ifdef TEST_BACKUPS_WITH_A_SHORT_PERIOD
   normalPeriod = 5;
   lockedPeriod = 5;
 #endif
@@ -113,6 +126,33 @@ void BackupObserver::backgroundThread()
               TRACE("RECO: Document '%d' backup was canceled by UI\n", doc->id());
               somethingLocked = true;
             }
+#ifdef TEST_BACKUP_INTEGRITY
+            else {
+              DocumentReader reader(doc, 500);
+              std::unique_ptr<app::Document> copy(
+                m_session->restoreBackupDocById(doc->id()));
+              DocumentDiff diff = compare_docs(doc, copy.get());
+              if (diff.anything) {
+                TRACE("RECO: Differences (%s/%s/%s/%s/%s/%s/%s)\n",
+                      diff.canvas ? "canvas": "",
+                      diff.totalFrames ? "totalFrames": "",
+                      diff.frameDuration ? "frameDuration": "",
+                      diff.frameTags ? "frameTags": "",
+                      diff.palettes ? "palettes": "",
+                      diff.layers ? "layers": "",
+                      diff.cels ? "cels": "");
+
+                app::Document* copyDoc = copy.release();
+                ui::execute_from_ui_thread(
+                  [this, copyDoc] {
+                    m_ctx->documents().add(copyDoc);
+                  });
+              }
+              else {
+                TRACE("RECO: No differences\n");
+              }
+            }
+#endif
           }
         }
         catch (const std::exception&) {
diff --git a/src/app/crash/session.cpp b/src/app/crash/session.cpp
index 5f372f33b..2252cb69a 100644
--- a/src/app/crash/session.cpp
+++ b/src/app/crash/session.cpp
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2001-2016  David Capello
+// Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
 // the End-User License Agreement for Aseprite.
@@ -189,19 +189,27 @@ void Session::removeDocument(app::Document* doc)
   }
 }
 
-void Session::restoreBackup(Backup* backup)
+app::Document* Session::restoreBackupDoc(const std::string& backupDir)
 {
   Console console;
   try {
-    app::Document* doc = read_document(backup->dir());
+    app::Document* doc = read_document(backupDir);
     if (doc) {
       fixFilename(doc);
-      UIContext::instance()->documents().add(doc);
+      return doc;
     }
   }
   catch (const std::exception& ex) {
     Console::showException(ex);
   }
+  return nullptr;
+}
+
+void Session::restoreBackup(Backup* backup)
+{
+  app::Document* doc = restoreBackupDoc(backup->dir());
+  if (doc)
+    UIContext::instance()->documents().add(doc);
 }
 
 void Session::restoreBackupById(const ObjectId id)
@@ -210,9 +218,18 @@ void Session::restoreBackupById(const ObjectId id)
   if (!base::is_directory(docDir))
     return;
 
-  base::UniquePtr<Backup> backup(new Backup(docDir));
-  if (backup)
-    restoreBackup(backup.get());
+  app::Document* doc = restoreBackupDoc(docDir);
+  if (doc)
+    UIContext::instance()->documents().add(doc);
+}
+
+app::Document* Session::restoreBackupDocById(const doc::ObjectId id)
+{
+  std::string docDir = base::join_path(m_path, base::convert_to<std::string>(int(id)));
+  if (!base::is_directory(docDir))
+    return nullptr;
+
+  return restoreBackupDoc(docDir);
 }
 
 void Session::restoreRawImages(Backup* backup, RawImagesAs as)
diff --git a/src/app/crash/session.h b/src/app/crash/session.h
index 2016a5800..f7553a92d 100644
--- a/src/app/crash/session.h
+++ b/src/app/crash/session.h
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2001-2016  David Capello
+// Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
 // the End-User License Agreement for Aseprite.
@@ -55,10 +55,12 @@ namespace crash {
 
     void restoreBackup(Backup* backup);
     void restoreBackupById(const doc::ObjectId id);
+    app::Document* restoreBackupDocById(const doc::ObjectId id);
     void restoreRawImages(Backup* backup, RawImagesAs as);
     void deleteBackup(Backup* backup);
 
   private:
+    app::Document* restoreBackupDoc(const std::string& backupDir);
     void loadPid();
     std::string pidFilename() const;
     std::string verFilename() const;
diff --git a/src/app/document.cpp b/src/app/document.cpp
index 4d963c0a7..8c87ea1d6 100644
--- a/src/app/document.cpp
+++ b/src/app/document.cpp
@@ -439,7 +439,7 @@ Document* Document::duplicate(DuplicateType type) const
 
   // Copy only some flags
   documentCopy->m_flags = (m_flags & kMaskVisible);
-
+  documentCopy->setFilename(filename());
   documentCopy->setMask(mask());
   documentCopy->generateMaskBoundaries();
 
diff --git a/src/app/document_diff.cpp b/src/app/document_diff.cpp
new file mode 100644
index 000000000..196ff85aa
--- /dev/null
+++ b/src/app/document_diff.cpp
@@ -0,0 +1,144 @@
+// Aseprite
+// Copyright (C) 2018  David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/document_diff.h"
+
+#include "app/document.h"
+#include "doc/cel.h"
+#include "doc/frame_tag.h"
+#include "doc/image.h"
+#include "doc/layer.h"
+#include "doc/palette.h"
+#include "doc/primitives.h"
+#include "doc/sprite.h"
+
+namespace app {
+
+DocumentDiff compare_docs(const Document* a,
+                          const Document* b)
+{
+  DocumentDiff diff;
+
+  // Don't compare filenames
+  //if (a->filename() != b->filename())...
+
+  // Compare sprite specs
+  if (a->sprite()->width() != b->sprite()->width() ||
+      a->sprite()->height() != b->sprite()->height() ||
+      a->sprite()->pixelFormat() != b->sprite()->pixelFormat()) {
+    diff.anything = diff.canvas = true;
+  }
+
+  // Frames layers
+  if (a->sprite()->totalFrames() != b->sprite()->totalFrames()) {
+    diff.anything = diff.totalFrames = true;
+  }
+  else {
+    for (frame_t f=0; f<a->sprite()->totalFrames(); ++f) {
+      if (a->sprite()->frameDuration(f) != b->sprite()->frameDuration(f)) {
+        diff.anything = diff.frameDuration = true;
+        break;
+      }
+    }
+  }
+
+  // Tags
+  if (a->sprite()->frameTags().size() != b->sprite()->frameTags().size()) {
+    diff.anything = diff.frameTags = true;
+  }
+  else {
+    auto aIt = a->sprite()->frameTags().begin(), aEnd = a->sprite()->frameTags().end();
+    auto bIt = b->sprite()->frameTags().begin(), bEnd = b->sprite()->frameTags().end();
+    for (; aIt != aEnd && bIt != bEnd; ++aIt, ++bIt) {
+      const FrameTag* aTag = *aIt;
+      const FrameTag* bTag = *bIt;
+      if (aTag->fromFrame() != bTag->fromFrame() ||
+          aTag->toFrame()   != bTag->toFrame()   ||
+          aTag->name()      != bTag->name()      ||
+          aTag->color()     != bTag->color()     ||
+          aTag->aniDir()    != bTag->aniDir()) {
+        diff.anything = diff.frameTags = true;
+      }
+    }
+  }
+
+  // Palettes layers
+  if (a->sprite()->getPalettes().size() != b->sprite()->getPalettes().size()) {
+    const PalettesList& aPals = a->sprite()->getPalettes();
+    const PalettesList& bPals = b->sprite()->getPalettes();
+    auto aIt = aPals.begin(), aEnd = aPals.end();
+    auto bIt = bPals.begin(), bEnd = bPals.end();
+
+    for (; aIt != aEnd && bIt != bEnd; ++aIt, ++bIt) {
+      const Palette* aPal = *aIt;
+      const Palette* bPal = *bIt;
+
+      if (aPal->countDiff(bPal, nullptr, nullptr)) {
+        diff.anything = diff.palettes = true;
+        break;
+      }
+    }
+  }
+
+  // Compare layers
+  if (a->sprite()->allLayersCount() != b->sprite()->allLayersCount()) {
+    diff.anything = diff.layers = true;
+  }
+  else {
+    LayerList aLayers = a->sprite()->allLayers();
+    LayerList bLayers = b->sprite()->allLayers();
+    auto aIt = aLayers.begin(), aEnd = aLayers.end();
+    auto bIt = bLayers.begin(), bEnd = bLayers.end();
+
+    for (; aIt != aEnd && bIt != bEnd; ++aIt, ++bIt) {
+      const Layer* aLay = *aIt;
+      const Layer* bLay = *bIt;
+
+      if (aLay->type() != bLay->type() ||
+          aLay->name() != bLay->name() ||
+          aLay->flags() != bLay->flags() ||
+          (aLay->isImage() && bLay->isImage() &&
+           (((const LayerImage*)aLay)->opacity() != ((const LayerImage*)bLay)->opacity()))) {
+        diff.anything = diff.layers = true;
+        break;
+      }
+
+      if (diff.totalFrames) {
+        for (frame_t f=0; f<a->sprite()->totalFrames(); ++f) {
+          const Cel* aCel = aLay->cel(f);
+          const Cel* bCel = bLay->cel(f);
+
+          if ((!aCel && bCel) ||
+              (aCel && !bCel)) {
+            diff.anything = diff.cels = true;
+          }
+          else if (aCel && bCel) {
+            if (aCel->frame() == bCel->frame() ||
+                aCel->bounds() == bCel->bounds() ||
+                aCel->opacity() == bCel->opacity()) {
+              diff.anything = diff.cels = true;
+            }
+            if (aCel->image() && bCel->image()) {
+              if (aCel->image()->bounds() != bCel->image()->bounds() ||
+                  count_diff_between_images(aCel->image(), bCel->image()))
+                diff.anything = diff.images = true;
+            }
+            else if (aCel->image() != bCel->image())
+              diff.anything = diff.images = true;
+          }
+        }
+      }
+    }
+  }
+
+  return diff;
+}
+
+} // namespace app
diff --git a/src/app/document_diff.h b/src/app/document_diff.h
new file mode 100644
index 000000000..9c8c592cf
--- /dev/null
+++ b/src/app/document_diff.h
@@ -0,0 +1,45 @@
+// Aseprite
+// Copyright (C) 2018  David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_DOCUMENT_DIFF_H_INCLUDED
+#define APP_DOCUMENT_DIFF_H_INCLUDED
+#pragma once
+
+namespace app {
+  class Document;
+
+  struct DocumentDiff {
+    bool anything : 1;
+    bool canvas : 1;
+    bool totalFrames : 1;
+    bool frameDuration : 1;
+    bool frameTags : 1;
+    bool palettes : 1;
+    bool layers : 1;
+    bool cels : 1;
+    bool images : 1;
+
+    DocumentDiff() :
+      anything(false),
+      canvas(false),
+      totalFrames(false),
+      frameDuration(false),
+      frameTags(false),
+      palettes(false),
+      layers(false),
+      cels(false),
+      images(false) {
+    }
+  };
+
+  // Useful for testing purposes to detect if two documents (after
+  // some kind of operation) are equivalent.
+  DocumentDiff compare_docs(const Document* a,
+                            const Document* b);
+
+} // namespace app
+
+#endif
diff --git a/src/ui/manager.cpp b/src/ui/manager.cpp
index 2e492adbe..23d99ab41 100644
--- a/src/ui/manager.cpp
+++ b/src/ui/manager.cpp
@@ -1,5 +1,5 @@
 // Aseprite UI Library
-// Copyright (C) 2001-2017  David Capello
+// Copyright (C) 2001-2018  David Capello
 //
 // This file is released under the terms of the MIT license.
 // Read LICENSE.txt for more information.
@@ -16,6 +16,7 @@
 
 #include "ui/manager.h"
 
+#include "base/concurrent_queue.h"
 #include "base/scoped_value.h"
 #include "base/time.h"
 #include "she/display.h"
@@ -72,6 +73,7 @@ static base::thread::native_handle_type manager_thread = 0;
 
 static WidgetsList mouse_widgets_list; // List of widgets to send mouse events
 static Messages msg_queue;             // Messages queue
+static base::concurrent_queue<Message*> concurrent_msg_queue;
 static Filters msg_filters[NFILTERS]; // Filters for every enqueued message
 static int filter_locks = 0;
 
@@ -264,6 +266,14 @@ bool Manager::generateMessages()
   if (children().empty())
     return false;
 
+  // Generate messages from other threads
+  if (!concurrent_msg_queue.empty()) {
+    Message* msg = nullptr;
+    while (concurrent_msg_queue.try_pop(msg))
+      msg_queue.push_back(msg);
+  }
+
+  // Generate messages from OS input
   generateMessagesFromSheEvents();
 
   // Generate messages for timers
@@ -643,7 +653,11 @@ void Manager::addToGarbage(Widget* widget)
 void Manager::enqueueMessage(Message* msg)
 {
   ASSERT(msg);
-  msg_queue.push_back(msg);
+
+  if (is_ui_thread())
+    msg_queue.push_back(msg);
+  else
+    concurrent_msg_queue.push(msg);
 }
 
 Window* Manager::getTopWindow()
@@ -1414,6 +1428,8 @@ bool Manager::sendMessageToWidget(Message* msg, Widget* widget)
 #ifdef REPORT_EVENTS
   {
     static const char* msg_name[] = {
+      "kFunctionMessage",
+
       "kOpenMessage",
       "kCloseMessage",
       "kCloseDisplayMessage",
diff --git a/src/ui/message.h b/src/ui/message.h
index f777f31e4..61337eba9 100644
--- a/src/ui/message.h
+++ b/src/ui/message.h
@@ -18,6 +18,8 @@
 #include "ui/pointer_type.h"
 #include "ui/widgets_list.h"
 
+#include <functional>
+
 namespace ui {
 
   class Timer;
@@ -82,6 +84,16 @@ namespace ui {
     KeyModifiers m_modifiers; // Key modifiers pressed when message was created
   };
 
+  class FunctionMessage : public Message {
+  public:
+    FunctionMessage(std::function<void()>&& f)
+      : Message(kFunctionMessage),
+        m_f(std::move(f)) { }
+    void call() { m_f(); }
+  private:
+    std::function<void()> m_f;
+  };
+
   class KeyMessage : public Message {
   public:
     KeyMessage(MessageType type,
diff --git a/src/ui/message_type.h b/src/ui/message_type.h
index 12f7e3fe2..3d4760fd5 100644
--- a/src/ui/message_type.h
+++ b/src/ui/message_type.h
@@ -1,5 +1,5 @@
 // Aseprite UI Library
-// Copyright (C) 2001-2016  David Capello
+// Copyright (C) 2001-2018  David Capello
 //
 // This file is released under the terms of the MIT license.
 // Read LICENSE.txt for more information.
@@ -13,6 +13,7 @@ namespace ui {
   // Message types.
   enum MessageType {
     // General messages.
+    kFunctionMessage, // Call a function from the UI thread.
     kOpenMessage,     // Windows is open.
     kCloseMessage,    // Windows is closed.
     kCloseDisplayMessage, // The user wants to close the entire application.
diff --git a/src/ui/system.cpp b/src/ui/system.cpp
index d92010872..94e2edd88 100644
--- a/src/ui/system.cpp
+++ b/src/ui/system.cpp
@@ -20,6 +20,7 @@
 #include "ui/intern.h"
 #include "ui/intern.h"
 #include "ui/manager.h"
+#include "ui/message.h"
 #include "ui/overlay.h"
 #include "ui/overlay_manager.h"
 #include "ui/scale.h"
@@ -346,6 +347,18 @@ void set_mouse_position(const gfx::Point& newPos)
   _internal_set_mouse_position(newPos);
 }
 
+void execute_from_ui_thread(std::function<void()>&& f)
+{
+  ASSERT(Manager::getDefault());
+
+  Manager* man = Manager::getDefault();
+  ASSERT(man);
+
+  FunctionMessage* msg = new FunctionMessage(std::move(f));
+  msg->addRecipient(man);
+  man->enqueueMessage(msg);
+}
+
 bool is_ui_thread()
 {
   return (main_gui_thread == base::this_thread::native_handle());
diff --git a/src/ui/system.h b/src/ui/system.h
index d9a6a9f60..5bc4071dd 100644
--- a/src/ui/system.h
+++ b/src/ui/system.h
@@ -13,6 +13,7 @@
 #include "ui/cursor_type.h"
 #include "ui/mouse_buttons.h"
 
+#include <functional>
 #include <string>
 
 namespace she { class Display; }
@@ -69,6 +70,7 @@ namespace ui {
   const gfx::Point& get_mouse_position();
   void set_mouse_position(const gfx::Point& newPos);
 
+  void execute_from_ui_thread(std::function<void()>&& f);
   bool is_ui_thread();
 #ifdef _DEBUG
   void assert_ui_thread();
diff --git a/src/ui/widget.cpp b/src/ui/widget.cpp
index c3445709e..7ab64bee7 100644
--- a/src/ui/widget.cpp
+++ b/src/ui/widget.cpp
@@ -1406,6 +1406,10 @@ bool Widget::onProcessMessage(Message* msg)
 
   switch (msg->type()) {
 
+    case kFunctionMessage:
+      static_cast<FunctionMessage*>(msg)->call();
+      break;
+
     case kOpenMessage:
     case kCloseMessage:
     case kWinMoveMessage: