Skip to content

Commit d259a22

Browse files
committed
json_data: Autorepair corrupted project files
If a file is detected as having corrupted unicode escapes, we make a backup copy as filename.osp.bak (or filename.osp.bak.1...), then regexp-replace all of the broken escapes with corrected ones. Then we run the file through json.loads() and json.dumps() again to remove the escaped characters, and write the results back to the original file location as UTF-8.
1 parent 7f8107a commit d259a22

File tree

1 file changed

+101
-8
lines changed

1 file changed

+101
-8
lines changed

src/classes/json_data.py

+101-8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from classes.assets import get_assets_path
3737
from classes.logger import log
3838
from classes import info
39+
from classes.app import get_app
3940

4041
# Compiled path regex
4142
path_regex = re.compile(r'\"(image|path)\":.*?\"(.*?)\"')
@@ -53,6 +54,24 @@ def __init__(self):
5354
self._data = {} # Private data store, accessible through the get and set methods
5455
self.data_type = "json data"
5556

57+
# Regular expression for project files with possible corruption
58+
self.version_re = re.compile(r'"openshot-qt".*"2.5.0')
59+
60+
# Regular expression matching likely corruption in project files
61+
self.damage_re = re.compile(r'/u([0-9a-fA-F]{4})')
62+
63+
# Connection to Qt main window, used in recovery alerts
64+
app = get_app()
65+
if app:
66+
self.app = app
67+
68+
if app and app._tr:
69+
self._ = app._tr
70+
else:
71+
def fn(arg):
72+
return arg
73+
self._ = fn
74+
5675
def get(self, key):
5776
""" Get copied value of a given key in data store """
5877
key = key.lower()
@@ -108,7 +127,7 @@ def merge_settings(self, default, user):
108127
# Update default values to match user values
109128
for item in default:
110129
user_value = user_values.get(item["setting"], None)
111-
if user_value != None:
130+
if user_value is not None:
112131
item["value"] = user_value
113132

114133
# Return merged list
@@ -129,16 +148,51 @@ def read_from_file(self, file_path, path_mode="ignore"):
129148
try:
130149
with open(file_path, 'r', encoding='utf-8') as f:
131150
contents = f.read()
132-
if contents:
133-
if path_mode == "absolute":
134-
# Convert any paths to absolute
135-
contents = self.convert_paths_to_absolute(file_path, contents)
136-
return json.loads(contents)
151+
if not contents:
152+
raise RuntimeError("Couldn't load {} file, no data.".format(self.data_type))
153+
154+
# Scan for and correct possible OpenShot 2.5.0 corruption
155+
if self.damage_re.search(contents) and self.version_re.search(contents):
156+
# File contains corruptions, backup and repair
157+
self.make_repair_backup(file_path, contents)
158+
159+
# Repair all corrupted escapes
160+
contents, subs_count = self.damage_re.subn(r'\\u\1', contents)
161+
162+
if subs_count < 1:
163+
# Nothing to do!
164+
log.info("No recovery substitutions on {}".format(file_path))
165+
else:
166+
# We have to de- and re-serialize the data, to complete repairs
167+
temp_data = json.loads(contents)
168+
contents = json.dumps(temp_data, ensure_ascii=False)
169+
temp_data = {}
170+
171+
# Save the repaired data back to the original file
172+
with open(file_path, "w", encoding="utf-8") as fout:
173+
fout.write(contents)
174+
175+
msg_log = "Repaired {} corruptions in file {}"
176+
msg_local = self._("Repaired {num} corruptions in file {path}")
177+
log.info(msg_log.format(subs_count, file_path))
178+
if hasattr(self.app, "window") and hasattr(self.app.window, "statusBar"):
179+
self.app.window.statusBar.showMessage(
180+
msg_local.format(num=subs_count, path=file_path), 5000
181+
)
182+
183+
# Process JSON data
184+
if path_mode == "absolute":
185+
# Convert any paths to absolute
186+
contents = self.convert_paths_to_absolute(file_path, contents)
187+
return json.loads(contents)
188+
except RuntimeError as ex:
189+
log.error(str(ex))
190+
raise
137191
except Exception as ex:
138192
msg = ("Couldn't load {} file: {}".format(self.data_type, ex))
139193
log.error(msg)
140-
raise Exception(msg)
141-
msg = ("Couldn't load {} file, no data.".format(self.data_type))
194+
raise Exception(msg) from ex
195+
msg = ()
142196
log.warning(msg)
143197
raise Exception(msg)
144198

@@ -261,3 +315,42 @@ def convert_paths_to_relative(self, file_path, previous_path, data):
261315
log.error("Error while converting absolute paths to relative paths: %s" % str(ex))
262316

263317
return data
318+
319+
def make_repair_backup(self, file_path, jsondata, backup_dir=None):
320+
""" Make a backup copy of an OSP file before performing recovery """
321+
322+
if backup_dir:
323+
backup_base = os.path.join(backup_dir, os.path.basename(file_path))
324+
else:
325+
backup_base = os.path.realpath(file_path)
326+
327+
# Generate a filename.osp.bak (or filename.osp.bak.1...) backup file name
328+
backup_file = "{}.bak".format(backup_base)
329+
dup_count = 1
330+
while os.path.exists(backup_file) and dup_count <= 999:
331+
backup_file = "{}.bak.{}".format(backup_base, dup_count)
332+
dup_count += 1
333+
334+
if dup_count >= 1000:
335+
# Something's wrong, we can't find a free save file name; bail
336+
raise Exception("Aborting recovery, cannot create backup file")
337+
338+
# Attempt to open backup file for writing and store original data
339+
try:
340+
with open(backup_file, "w") as fout:
341+
fout.write(jsondata)
342+
343+
if hasattr(self.app, "window") and hasattr(self.app.window, "statusBar"):
344+
self.app.window.statusBar.showMessage(
345+
self._("Saved backup file {}").format(backup_file), 5000
346+
)
347+
log.info("Backed up {} as {}".format(file_path, backup_file))
348+
except (PermissionError, FileExistsError) as ex:
349+
# Couldn't write to backup file! Try alternate location
350+
log.error("Couldn't write to backup file {}: {}".format(backup_file, ex))
351+
if not backup_dir:
352+
# Retry in alternate location
353+
log.info("Attempting to save backup in user config directory")
354+
self.make_repair_backup(file_path, jsondata, backup_dir=info.USER_PATH)
355+
else:
356+
raise Exception("Aborting recovery, cannot write backup file") from ex

0 commit comments

Comments
 (0)