1 /*
2  * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3  *
4  * Solarus Quest Editor is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Solarus Quest Editor is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 #include "editor_exception.h"
18 #include "file_tools.h"
19 #include <solarus/core/Common.h>
20 #include <QApplication>
21 #include <QDebug>
22 #include <QDir>
23 #include <QFile>
24 #include <QFileInfo>
25 #include <QRegularExpression>
26 #include <QTextStream>
27 
28 #include <QDebug>
29 
30 namespace SolarusEditor {
31 
32 namespace FileTools {
33 
34 namespace {
35   bool assets_path_initialized = false;
36   QString assets_path;
37 }
38 
39 /**
40  * @brief Determines the path to the Solarus Quest Editor assets directory.
41  *
42  * The directory "assets" is searched in the following paths in this order:
43  * - The directory containing the executable.
44  * - The source path (macro SOLARUSEDITOR_SOURCE_PATH)
45  *   (useful for developer builds).
46  * - The install path (macro SOLARUSEDITOR_DATADIR_PATH).
47  */
initialize_assets()48 void initialize_assets() {
49 
50   const QString& executable_path = QCoreApplication::applicationDirPath();
51   QStringList potential_paths;
52 
53   // Try the current directory first.
54   potential_paths << executable_path + "/assets";
55 
56   // Try the source path if we are not running the installed executable.
57   bool running_installed_executable = (executable_path == SOLARUSEDITOR_BINDIR_PATH);
58 #ifdef SOLARUSEDITOR_SOURCE_PATH
59   if (!running_installed_executable) {
60     potential_paths << SOLARUSEDITOR_SOURCE_PATH "/assets";
61   }
62 #endif
63 
64   // Try the install path if we are running the installed executable.
65 #ifdef SOLARUSEDITOR_DATADIR_PATH
66   if (running_installed_executable) {
67     potential_paths << SOLARUSEDITOR_DATADIR_PATH "/assets";
68   }
69 #endif
70 
71   assets_path_initialized = true;
72   for (const QString& potential_path : potential_paths) {
73     if (QFile(potential_path).exists()) {
74       assets_path = potential_path;
75       return;
76     }
77   }
78 }
79 
80 /**
81  * @brief Returns the path to the Solarus Quest Editor assets directrory.
82  * @return The assets path or an empty string if assets could not be found.
83  */
get_assets_path()84 QString get_assets_path() {
85 
86   if (!assets_path_initialized) {
87     initialize_assets();
88   }
89   return assets_path;
90 }
91 
92 /**
93  * @brief Utility function to copy a file or directory with its content.
94  * @param src The file or directory to copy.
95  * @param dst The destination file path. It should be the name of the file
96  * or directory to create.
97  * @throws EditorException if the copy failed. In this case, files already
98  * successfully copied are left.
99  */
copy_recursive(const QString & src,const QString & dst)100 void copy_recursive(const QString& src, const QString& dst) {
101 
102   if (src == dst) {
103     throw EditorException(QApplication::tr("Source and destination are the same: '%1'").arg(src));
104   }
105 
106   QFileInfo src_info(src);
107   QFileInfo dst_info(dst);
108 
109   if (!src_info.exists()) {
110     throw EditorException(QApplication::tr("No such file or folder: '%1'").arg(src));
111   }
112 
113   if (!src_info.isReadable()) {
114     throw EditorException(QApplication::tr("Source file cannot be read: '%1'").arg(src));
115   }
116 
117   if (dst_info.exists()) {
118     throw EditorException(QApplication::tr("Destination already exists: '%1'").arg(dst));
119   }
120 
121   if (src_info.isDir()) {
122 
123     QDir dst_dir(dst);
124     dst_dir.cdUp();
125 
126     if (!dst_dir.exists()) {
127       throw EditorException(QApplication::tr("No such folder: '%1'").arg(dst_dir.path()));
128     }
129 
130     QString src_canonical_path = src_info.canonicalFilePath();
131     QString dst_parent_canonical_path = dst_dir.canonicalPath();
132 
133     if (dst_parent_canonical_path.startsWith(src_canonical_path)) {
134       throw EditorException(QApplication::tr("Cannot copy folder '%1' to one of its own subfolders: '%2'").arg(src, dst));
135     }
136 
137     if (!dst_dir.mkdir(dst_info.fileName())) {
138       throw EditorException(QApplication::tr("Cannot create folder '%1'").arg(dst));
139     }
140 
141     QDir src_dir(src);
142     const QStringList& file_names = src_dir.entryList(
143           QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
144     for (const QString& file_name : file_names) {
145       QString next_src = src + '/' + file_name;
146       QString next_dst = dst + '/' + file_name;
147       copy_recursive(next_src, next_dst);
148     }
149   }
150   else {
151     if (!QFile::copy(src, dst)) {
152       throw EditorException(QApplication::tr("Cannot copy file '%1' to '%2'").arg(src, dst));
153     }
154 
155     if (src.startsWith(":/")) {
156       // Files from Qt resources are read-only. Set usual permissions now.
157       QFile::setPermissions(dst,
158                             QFile::ReadUser | QFile::WriteUser | QFile::ExeUser |
159                             QFile::ReadGroup| QFile::ExeGroup |
160                             QFile::ReadOther| QFile::ExeOther);
161     }
162   }
163 }
164 
165 /**
166  * @brief Deletes a file or a directory with its content.
167  *
168  * Does nothing if the file or directory does not exist.
169  *
170  * @param path The file or directory to delete.
171  * @throws EditorException if the deletion failed.
172  */
delete_recursive(const QString & path)173 void delete_recursive(const QString& path) {
174 
175   QFileInfo info(path);
176   if (!info.exists()) {
177     return;
178   }
179 
180   if (!info.isDir()) {
181     // Not a directory.
182     if (!QFile::remove(path)) {
183       throw EditorException(QApplication::tr("Failed to delete file '%1'").arg(path));
184     }
185   }
186   else {
187     // Directory.
188     QDir dir(path);
189     const QStringList& file_names = dir.entryList(
190           QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
191     for (const QString& file_name : file_names) {
192       QString child_path = path + '/' + file_name;
193       delete_recursive(child_path);
194     }
195 
196     if (!QDir().rmdir(path)) {
197       throw EditorException(QApplication::tr("Failed to delete folder '%1'").arg(path));
198     }
199   }
200 }
201 
202 /**
203  * @brief Makes sure that the specified directory exists.
204  *
205  * Creates necessary parents directories if needed.
206  *
207  * @param path A directory path.
208  * @throws EditorException In case of error.
209  */
create_directories(const QString & path)210 void create_directories(const QString& path) {
211 
212   bool success = QDir().mkpath(path);
213   if (!success) {
214     throw EditorException(QApplication::tr("Cannot create folder '%1'").arg(path));
215   }
216 }
217 
218 /**
219  * @brief Replaces all occurences of the given pattern in a file.
220  * @param path Path of the file to modify.
221  * @param regexp The pattern to replace.
222  * @param replacement The string to put instead of the pattern.
223  * @param replace_all @c true to replace all occurences, @c false to only
224  * replace the first one.
225  * @return @c true if there was a change.
226  * @throws EditorException In case of error.
227  */
replace_in_file(const QString & path,const QRegularExpression & regex,const QString & replacement,bool replace_all)228 bool replace_in_file(
229     const QString& path,
230     const QRegularExpression& regex,
231     const QString& replacement,
232     bool replace_all
233 ) {
234   QFile file(path);
235 
236   if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
237     throw EditorException(QApplication::tr("Cannot open file '%1'").arg(file.fileName()));
238   }
239   QTextStream in(&file);
240   in.setCodec("UTF-8");
241   QString content = in.readAll();
242   file.close();
243 
244   QString old_content = content;
245   if (replace_all) {
246     content.replace(regex, replacement);
247   } else {
248     QRegularExpressionMatch match = regex.match(content);
249     if (match.hasMatch()) {
250       content.replace(match.capturedStart(), match.capturedLength(), replacement);
251     }
252   }
253 
254   if (content == old_content) {
255     // No change.
256     return false;
257   }
258 
259   if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
260     throw EditorException(QApplication::tr("Cannot open file '%1' for writing").arg(file.fileName()));
261   }
262   QTextStream out(&file);
263   out.setCodec("UTF-8");
264   out << content;
265   file.close();
266   return true;
267 }
268 
269 }  // namespace FileTools
270 
271 }  // namespace SolarusEditor
272