Add hash check on document read/write (#890)
* Add sha256sum function * Add hash check to document class * Make sha256sum test more thorough * Handle exceptions in the sha256sum function directly * Update test coverage * Clarify title on dialog box * Don't write blank lines in makeAlert
This commit is contained in:
committed by
GitHub
parent
d65c5e93b6
commit
c852a5bda3
+25
-1
@@ -24,6 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
@@ -411,7 +412,7 @@ def jsonEncode(data, n=0, nmax=0):
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Other Functions
|
||||
# File and File System Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def makeFileNameSafe(theText):
|
||||
@@ -424,6 +425,29 @@ def makeFileNameSafe(theText):
|
||||
return cleanName
|
||||
|
||||
|
||||
def sha256sum(filePath):
|
||||
"""Make a shasum of a file using a buffer.
|
||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
||||
"""
|
||||
hDigest = hashlib.sha256()
|
||||
bData = bytearray(65536)
|
||||
mData = memoryview(bData)
|
||||
try:
|
||||
with open(filePath, mode="rb", buffering=0) as inFile:
|
||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
||||
hDigest.update(mData[:n])
|
||||
except Exception:
|
||||
logger.error("Could not read sha256sum of: %s", filePath)
|
||||
logException()
|
||||
return None
|
||||
|
||||
return hDigest.hexdigest()
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Other Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def getGuiItem(theName):
|
||||
"""Returns a QtWidget based on its objectName.
|
||||
"""
|
||||
|
||||
@@ -27,7 +27,7 @@ import os
|
||||
import logging
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||
from novelwriter.common import isHandle
|
||||
from novelwriter.common import isHandle, sha256sum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,6 +44,8 @@ class NWDoc():
|
||||
self._fileLoc = None # The file location of the currently open item
|
||||
self._docMeta = {} # The meta data of the currently open item
|
||||
self._docError = "" # The latest encountered IO error
|
||||
self._prevHash = None # Previous sha256sum of the document file
|
||||
self._currHash = None # Latest sha256sum of the document file
|
||||
|
||||
if isHandle(theHandle):
|
||||
self._docHandle = theHandle
|
||||
@@ -80,6 +82,8 @@ class NWDoc():
|
||||
|
||||
theText = ""
|
||||
self._docMeta = {}
|
||||
self._prevHash = sha256sum(docPath)
|
||||
|
||||
if os.path.isfile(docPath):
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -108,7 +112,7 @@ class NWDoc():
|
||||
|
||||
return theText
|
||||
|
||||
def writeDocument(self, docText):
|
||||
def writeDocument(self, docText, forceWrite=False):
|
||||
"""Write the document. The file is saved via a temp file in case
|
||||
of save failure. Returns True if successful, False if not.
|
||||
"""
|
||||
@@ -125,7 +129,13 @@ class NWDoc():
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
||||
|
||||
# DocMeta line
|
||||
if self._prevHash is not None and not forceWrite:
|
||||
self._currHash = sha256sum(docPath)
|
||||
if self._currHash is not None and self._currHash != self._prevHash:
|
||||
logger.error("File has been altered on disk since opened")
|
||||
return False
|
||||
|
||||
# DocMeta Line
|
||||
if self._theItem is None:
|
||||
docMeta = ""
|
||||
else:
|
||||
@@ -149,6 +159,9 @@ class NWDoc():
|
||||
os.unlink(docPath)
|
||||
os.rename(docTemp, docPath)
|
||||
|
||||
self._prevHash = sha256sum(docPath)
|
||||
self._currHash = self._prevHash
|
||||
|
||||
return True
|
||||
|
||||
def deleteDocument(self):
|
||||
|
||||
@@ -453,9 +453,23 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
self.saveCursorPosition()
|
||||
if not self._nwDocument.writeDocument(docText):
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Could not save document."), self._nwDocument.getError()
|
||||
], nwAlert.ERROR)
|
||||
saveOk = False
|
||||
if self._nwDocument._currHash != self._nwDocument._prevHash:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
self.tr("File Changed on Disk"),
|
||||
self.tr(
|
||||
"This document has been changed outside of novelWriter "
|
||||
"while it was open. Overvrite the file on disk?"
|
||||
)
|
||||
)
|
||||
if msgYes:
|
||||
saveOk = self._nwDocument.writeDocument(docText, forceWrite=True)
|
||||
|
||||
if not saveOk:
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Could not save document."), self._nwDocument.getError()
|
||||
], nwAlert.ERROR)
|
||||
|
||||
return False
|
||||
|
||||
self.setDocumentChanged(False)
|
||||
|
||||
@@ -1121,6 +1121,7 @@ class GuiMain(QMainWindow):
|
||||
can be either a string or an array of strings.
|
||||
"""
|
||||
if isinstance(theMessage, list):
|
||||
theMessage = list(filter(None, theMessage))
|
||||
popMsg = "<br>".join(theMessage)
|
||||
logMsg = theMessage
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user