]> git.lyx.org Git - lyx.git/blob - src/support/filetools.cpp
Update Win installer for new dictionary links. Untested.
[lyx.git] / src / support / filetools.cpp
1 /**
2  * \file filetools.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * parts Copyright 1985, 1990, 1993 Free Software Foundation, Inc.
7  *
8  * \author Ivan Schreter
9  * \author Dirk Niggemann
10  * \author Asger Alstrup
11  * \author Lars Gullik Bjønnes
12  * \author Jean-Marc Lasgouttes
13  * \author Angus Leeming
14  * \author John Levon
15  * \author Herbert Voß
16  *
17  * Full author contact details are available in file CREDITS.
18  *
19  * General path-mangling functions
20  */
21
22 #include <config.h>
23
24 #include "support/filetools.h"
25
26 #include "LyX.h"
27 #include "LyXRC.h"
28
29 #include "support/convert.h"
30 #include "support/debug.h"
31 #include "support/environment.h"
32 #include "support/gettext.h"
33 #include "support/lstrings.h"
34 #include "support/os.h"
35 #include "support/Messages.h"
36 #include "support/Package.h"
37 #include "support/PathChanger.h"
38 #include "support/Systemcall.h"
39 #include "support/qstring_helpers.h"
40 #include "support/TempFile.h"
41 #include "support/textutils.h"
42
43 #include <QDir>
44 #include <QUrl>
45
46 #include "support/lassert.h"
47
48 #include <fcntl.h>
49 #ifdef HAVE_MAGIC_H
50 #include <magic.h>
51 #endif
52 #ifdef HAVE_UNISTD_H
53 #include <unistd.h>
54 #endif
55
56 #include <cerrno>
57 #include <climits>
58 #include <cstdlib>
59 #include <cstdio>
60
61 #include <utility>
62 #include <fstream>
63 #include <regex>
64 #include <sstream>
65 #include <vector>
66
67 #include <QCryptographicHash>
68
69 #if defined (_WIN32)
70 #include <io.h>
71 #include <windows.h>
72 #endif
73
74 using namespace std;
75
76 #define USE_QPROCESS
77
78 namespace lyx {
79 namespace support {
80
81 bool isLyXFileName(string const & filename)
82 {
83         return suffixIs(ascii_lowercase(filename), ".lyx");
84 }
85
86
87 bool isSGMLFileName(string const & filename)
88 {
89         return suffixIs(ascii_lowercase(filename), ".sgml");
90 }
91
92
93 bool isValidLaTeXFileName(string const & filename)
94 {
95         string const invalid_chars("#%\"");
96         return filename.find_first_of(invalid_chars) == string::npos;
97 }
98
99
100 bool isValidDVIFileName(string const & filename)
101 {
102         string const invalid_chars("${}()[]^");
103         return filename.find_first_of(invalid_chars) == string::npos;
104 }
105
106
107 bool isBinaryFile(FileName const & filename)
108 {
109         bool isbinary = false;
110         if (filename.empty() || !filename.exists())
111                 return isbinary;
112
113 #ifdef HAVE_MAGIC_H
114         magic_t magic_cookie = magic_open(MAGIC_MIME_ENCODING);
115         if (magic_cookie) {
116                 bool detected = true;
117                 if (magic_load(magic_cookie, nullptr) != 0) {
118                         LYXERR(Debug::FILES, "isBinaryFile: "
119                                 "Could not load magic database - "
120                                 << magic_error(magic_cookie));
121                         detected = false;
122                 } else {
123                         char const *charset = magic_file(magic_cookie,
124                                         filename.toFilesystemEncoding().c_str());
125                         isbinary = contains(charset, "binary");
126                 }
127                 magic_close(magic_cookie);
128                 if (detected)
129                         return isbinary;
130         }
131 #endif
132         // Try by looking for binary chars at the beginning of the file.
133         // Note that this is formally not correct, since count_bin_chars
134         // expects utf8, and the passed string can be anything: plain text
135         // in any encoding, or really binary data. In practice it works,
136         // since QString::fromUtf8() drops invalid utf8 sequences, and
137         // while the exact number may not be correct, we still get a high
138         // number for truly binary files.
139
140         ifstream ifs(filename.toFilesystemEncoding().c_str());
141         if (!ifs)
142                 return isbinary;
143
144         // Maximum strings to read
145         int const max_count = 50;
146
147         // Maximum number of binary chars allowed
148         int const max_bin = 5;
149
150         int count = 0;
151         int binchars = 0;
152         string str;
153         while (count++ < max_count && !ifs.eof()) {
154                 getline(ifs, str);
155                 binchars += count_bin_chars(str);
156         }
157         return binchars > max_bin;
158 }
159
160
161 string const latex_path(string const & original_path,
162                 latex_path_extension extension,
163                 latex_path_dots dots)
164 {
165         // On cygwin, we may need windows or posix style paths.
166         string path = os::latex_path(original_path);
167         path = subst(path, "~", "\\string~");
168         if (path.find(' ') != string::npos) {
169                 // We can't use '"' because " is sometimes active (e.g. if
170                 // babel is loaded with the "german" option)
171                 if (extension == EXCLUDE_EXTENSION) {
172                         // changeExtension calls os::internal_path internally
173                         // so don't use it to remove the extension.
174                         string const ext = getExtension(path);
175                         string const base = ext.empty() ?
176                                 path :
177                                 path.substr(0, path.length() - ext.length() - 1);
178                         // changeExtension calls os::internal_path internally
179                         // so don't use it to re-add the extension.
180                         path = "\\string\"" + base + "\\string\"." + ext;
181                 } else {
182                         path = "\\string\"" + path + "\\string\"";
183                 }
184         }
185
186         if (dots != ESCAPE_DOTS)
187                 return path;
188
189         // Replace dots with the lyxdot macro, but only in the file name,
190         // not the directory part.
191         // addName etc call os::internal_path internally
192         // so don't use them for path manipulation
193         // The directory separator is always '/' for LaTeX.
194         string::size_type pos = path.rfind('/');
195         if (pos == string::npos)
196                 return subst(path, ".", "\\lyxdot ");
197         return path.substr(0, pos) + subst(path.substr(pos), ".", "\\lyxdot ");
198 }
199
200
201 // Substitutes spaces with underscores in filename (and path)
202 FileName const makeLatexName(FileName const & file)
203 {
204         string name = file.onlyFileName();
205         string const path = file.onlyPath().absFileName() + "/";
206
207         // ok so we scan through the string twice, but who cares.
208         // FIXME: in Unicode time this will break for sure! There is
209         // a non-latin world out there...
210         string const keep = "abcdefghijklmnopqrstuvwxyz"
211                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
212                 "@!'()*+,-./0123456789:;<=>?[]`|";
213
214         string::size_type pos = 0;
215         while ((pos = name.find_first_not_of(keep, pos)) != string::npos)
216                 name[pos++] = '_';
217
218         FileName latex_name(path + name);
219         latex_name.changeExtension(".tex");
220         return latex_name;
221 }
222
223
224 string const quoteName(string const & name, quote_style style)
225 {
226         switch(style) {
227         case quote_shell:
228                 // This does not work on native Windows for filenames
229                 // containing the following characters < > : " / \ | ? *
230                 // Moreover, it can't be made to work, as, according to
231                 // http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
232                 // those are reserved characters, and thus are forbidden.
233                 // Please, also note that the command-line parser in
234                 // ForkedCall::generateChild cannot deal with filenames
235                 // containing " or ', therefore we don't pass user filenames
236                 // to child processes if possible. We store them in a python
237                 // script instead, where we don't have these limitations.
238 #ifndef USE_QPROCESS
239                 return (os::shell() == os::UNIX) ?
240                         '\'' + subst(name, "'", "\'\\\'\'") + '\'' :
241                         '"' + name + '"';
242 #else
243                 // According to the QProcess parser, a single double
244                 // quote is represented by three consecutive ones.
245                 // Here we simply escape the double quote and let our
246                 // simple parser in Systemcall.cpp do the substitution.
247                 return '"' + subst(name, "\"", "\\\"") + '"';
248 #endif
249         case quote_shell_filename:
250                 return quoteName(os::external_path(name), quote_shell);
251         case quote_python:
252                 return "\"" + subst(subst(name, "\\", "\\\\"), "\"", "\\\"")
253                      + "\"";
254         }
255         // shut up stupid compiler
256         return string();
257 }
258
259
260 #if 0
261 // Uses a string of paths separated by ";"s to find a file to open.
262 // Can't cope with pathnames with a ';' in them. Returns full path to file.
263 // If path entry begins with $$LyX/, use system_lyxdir
264 // If path entry begins with $$User/, use user_lyxdir
265 // Example: "$$User/doc;$$LyX/doc"
266 FileName const fileOpenSearch(string const & path, string const & name,
267                              string const & ext)
268 {
269         FileName real_file;
270         string path_element;
271         bool notfound = true;
272         string tmppath = split(path, path_element, ';');
273
274         while (notfound && !path_element.empty()) {
275                 path_element = os::internal_path(path_element);
276                 if (!suffixIs(path_element, '/'))
277                         path_element += '/';
278                 path_element = subst(path_element, "$$LyX",
279                                      package().system_support().absFileName());
280                 path_element = subst(path_element, "$$User",
281                                      package().user_support().absFileName());
282
283                 real_file = fileSearch(path_element, name, ext);
284
285                 if (real_file.empty()) {
286                         do {
287                                 tmppath = split(tmppath, path_element, ';');
288                         } while (!tmppath.empty() && path_element.empty());
289                 } else {
290                         notfound = false;
291                 }
292         }
293         return real_file;
294 }
295 #endif
296
297
298 // Returns the real name of file name in directory path, with optional
299 // extension ext.
300 FileName const fileSearch(string const & path, string const & name,
301                           string const & exts, search_mode mode)
302 {
303         // if `name' is an absolute path, we ignore the setting of `path'
304         // Expand Environmentvariables in 'name'
305         string const tmpname = replaceEnvironmentPath(name);
306         FileName fullname(makeAbsPath(tmpname, path));
307         // search first without extension, then with it.
308         if (fullname.isReadableFile())
309                 return fullname;
310         if (exts.empty())
311                 // We are done.
312                 return mode == may_not_exist ? fullname : FileName();
313         int n = 0;
314         string ext = token(exts, ',', n);
315         while (!ext.empty()) {
316                 // Only add the extension if it is not already the extension of
317                 // fullname.
318                 bool addext = getExtension(fullname.absFileName()) != ext;
319                 if (addext) {
320                         if (mode == check_hidpi) {
321                                 FileName fullname2x = FileName(addExtension(fullname.absFileName() + "@2x", ext));
322                                 if (fullname2x.isReadableFile())
323                                         return fullname2x;
324                         }
325                         fullname = FileName(addExtension(fullname.absFileName(), ext));
326                 }
327                 if (fullname.isReadableFile() || mode == may_not_exist)
328                         return fullname;
329                 if (addext)
330                         fullname.changeExtension("");
331                 ext = token(exts, ',', ++n);
332         }
333         return FileName();
334 }
335
336
337 // Search the file name.ext in the subdirectory dir of
338 //   1) user_lyxdir
339 //   2) build_lyxdir (if not empty)
340 //   3) system_lyxdir
341 FileName const libFileSearch(string const & dir, string const & name,
342                            string const & ext, search_mode mode,
343                            bool const only_global)
344 {
345         FileName fullname;
346         if (!only_global) {
347                 fullname = fileSearch(addPath(package().user_support().absFileName(), dir),
348                                              name, ext, mode);
349                 if (!fullname.empty())
350                         return fullname;
351         }
352
353         if (!package().build_support().empty())
354                 fullname = fileSearch(addPath(package().build_support().absFileName(), dir),
355                                       name, ext, mode);
356         if (!fullname.empty())
357                 return fullname;
358
359         return fileSearch(addPath(package().system_support().absFileName(), dir),
360                                       name, ext, mode);
361 }
362
363
364 FileName const i18nLibFileSearch(string const & dir, string const & name,
365                   string const & ext)
366 {
367         // if the LANGUAGE variable is set, use it as a fallback for searching for files.
368         string lang = getGuiMessages().language();
369         string const language = getEnv("LANGUAGE");
370         if (!language.empty())
371                 lang += ":" + language;
372
373         for (auto const & l : getVectorFromString(lang, ":")) {
374                 FileName tmp;
375                 // First try with the full name
376                 // `en' files are not in a subdirectory
377                 if (l == "en")
378                         tmp = libFileSearch(dir, name, ext);
379                 else
380                         tmp = libFileSearch(addPath(dir, l), name, ext);
381                 if (!tmp.empty())
382                         return tmp;
383
384                 // Then the name without country code
385                 string const shortl = token(l, '_', 0);
386                 if (shortl != l) {
387                         tmp = libFileSearch(addPath(dir, shortl), name, ext);
388                         if (!tmp.empty())
389                                 return tmp;
390                 }
391         }
392
393         return libFileSearch(dir, name, ext);
394 }
395
396
397 FileName const imageLibFileSearch(string & dir, string const & name,
398                   string const & ext, search_mode mode)
399 {
400         if (!lyx::lyxrc.icon_set.empty()) {
401                 string const imagedir = addPath(dir, lyx::lyxrc.icon_set);
402                 FileName const fn = libFileSearch(imagedir, name, ext, mode);
403                 if (fn.exists()) {
404                         dir = imagedir;
405                         return fn;
406                 }
407         }
408         return libFileSearch(dir, name, ext, mode);
409 }
410
411
412 string const commandPrep(string const & command_in)
413 {
414         static string const token_scriptpath = "$$s/";
415         string const python_call = os::python();
416
417         string command = command_in;
418         if (prefixIs(command_in, python_call))
419                 command = os::python() + command_in.substr(python_call.length());
420
421         // Find the starting position of "$$s/"
422         string::size_type const pos1 = command.find(token_scriptpath);
423         if (pos1 == string::npos)
424                 return command;
425         // Find the end of the "$$s/some_subdir/some_script" word within
426         // command. Assumes that the script name does not contain spaces.
427         string::size_type const start_script = pos1 + 4;
428         string::size_type const pos2 = command.find(' ', start_script);
429         string::size_type const size_script = pos2 == string::npos?
430                 (command.size() - start_script) : pos2 - start_script;
431
432         // Does this script file exist?
433         string const script =
434                 libFileSearch(".", command.substr(start_script, size_script)).absFileName();
435
436         if (script.empty()) {
437                 // Replace "$$s/" with ""
438                 command.erase(pos1, 4);
439         } else {
440                 quote_style style = quote_shell;
441                 if (prefixIs(command, os::python()))
442                         style = quote_python;
443
444                 // Replace "$$s/foo/some_script" with "<path to>/some_script".
445                 string::size_type const size_replace = size_script + 4;
446                 command.replace(pos1, size_replace, quoteName(script, style));
447         }
448
449         return command;
450 }
451
452
453 FileName const tempFileName(FileName const & tempdir, string const & mask, bool const dir)
454 {
455         return tempFileName(TempFile(tempdir, mask).name(), dir);
456 }
457
458
459 FileName const tempFileName(string const & mask, bool const dir)
460 {
461         return tempFileName(TempFile(mask).name(), dir);
462 }
463
464
465 FileName const tempFileName(FileName tempfile, bool const dir)
466 {
467         // Since the QTemporaryFile object is destroyed at function return
468         // (which is what is intended here), the next call to this function
469         // may return the same file name again.
470         // Thus, in order to prevent race conditions, we track returned names
471         // and create our own unique names if QTemporaryFile returns a name again.
472         if (tmp_names_.find(tempfile.absFileName()) == tmp_names_.end()) {
473                 tmp_names_.insert(tempfile.absFileName());
474                 return tempfile;
475         }
476
477         // OK, we need another name. Simply append digits.
478         FileName tmp = tempfile;
479         string ext;
480         if (!dir) {
481                 // Store and remove extensions
482                 ext = "." + tempfile.extension();
483                 tmp.changeExtension("");
484         }
485         for (int i = 1; i < INT_MAX ;++i) {
486                 // Append digit to filename and re-add extension
487                 string const new_fn =
488                         tmp.absFileName() + convert<string>(i) + ext;
489                 if (tmp_names_.find(new_fn) == tmp_names_.end()) {
490                         tmp_names_.insert(new_fn);
491                         tempfile.set(new_fn);
492                         return tempfile;
493                 }
494         }
495
496         // This should not happen!
497         LYXERR0("tempFileName(): Could not create unique temp file name!");
498         return tempfile;
499 }
500
501
502 void removeTempFile(FileName const & fn)
503 {
504         if (!fn.exists())
505                 return;
506
507         string const abs = fn.absFileName();
508         tmp_names_.erase(abs);
509         fn.removeFile();
510 }
511
512
513 static FileName createTmpDir(FileName const & tempdir, string const & mask)
514 {
515         LYXERR(Debug::FILES, "createTmpDir: tempdir=`" << tempdir << "'\n"
516                 << "createTmpDir:    mask=`" << mask << '\'');
517
518         QFileInfo tmp_fi(QDir(toqstr(tempdir.absFileName())), toqstr(mask));
519         FileName const tmpfl =
520                 tempFileName(FileName(fromqstr(tmp_fi.absolutePath())),
521                              fromqstr(tmp_fi.fileName()) + ".XXXXXXXXXXXX", true);
522
523         if (tmpfl.empty() || !tmpfl.createDirectory(0700)) {
524                 LYXERR0("LyX could not create temporary directory in " << tempdir
525                         << "'");
526                 return FileName();
527         }
528
529         return tmpfl;
530 }
531
532
533 FileName const createLyXTmpDir(FileName const & deflt)
534 {
535         if (deflt.empty() || deflt == package().system_temp_dir())
536                 return createTmpDir(package().system_temp_dir(), "lyx_tmpdir");
537
538         if (deflt.createDirectory(0777))
539                 return deflt;
540
541         if (deflt.isDirWritable()) {
542                 // deflt could not be created because it
543                 // did exist already, so let's create our own
544                 // dir inside deflt.
545                 return createTmpDir(deflt, "lyx_tmpdir");
546         } else {
547                 // some other error occurred.
548                 return createTmpDir(package().system_temp_dir(), "lyx_tmpdir");
549         }
550 }
551
552
553 // Strip filename from path name
554 string const onlyPath(string const & filename)
555 {
556         // If empty filename, return empty
557         if (filename.empty())
558                 return filename;
559
560         // Find last / or start of filename
561         size_t j = filename.rfind('/');
562         return j == string::npos ? "./" : filename.substr(0, j + 1);
563 }
564
565
566 // Convert relative path into absolute path based on a basepath.
567 // If relpath is absolute, just use that.
568 // If basepath is empty, use CWD as base.
569 // Note that basePath can be a relative path, in the sense that it may
570 // not begin with "/" (e.g.), but it should NOT contain such constructs
571 // as "/../".
572 // FIXME It might be nice if the code didn't simply assume that.
573 FileName const makeAbsPath(string const & relPath, string const & basePath)
574 {
575         // checks for already absolute path
576         if (FileName::isAbsolute(relPath))
577                 return FileName(relPath);
578
579         // Copies given paths
580         string tempRel = os::internal_path(relPath);
581         // Since TempRel is NOT absolute, we can safely replace "//" with "/"
582         tempRel = subst(tempRel, "//", "/");
583
584         string tempBase;
585
586         if (FileName::isAbsolute(basePath))
587                 tempBase = basePath;
588         else
589                 tempBase = addPath(FileName::getcwd().absFileName(), basePath);
590
591         // Handle /./ at the end of the path
592         while (suffixIs(tempBase, "/./"))
593                 tempBase.erase(tempBase.length() - 2);
594
595         // processes relative path
596         string rTemp = tempRel;
597         string temp;
598
599         // Check for a leading "~"
600         // Split by first /
601         rTemp = split(rTemp, temp, '/');
602         if (temp == "~") {
603                 tempBase = Package::get_home_dir().absFileName();
604                 tempRel = rTemp;
605         }
606
607         rTemp = tempRel;
608         while (!rTemp.empty()) {
609                 // Split by next /
610                 rTemp = split(rTemp, temp, '/');
611
612                 if (temp == ".") continue;
613                 if (temp == "..") {
614                         // Remove one level of TempBase
615                         if (tempBase.length() <= 1) {
616                                 //this is supposed to be an absolute path, so...
617                                 tempBase = "/";
618                                 continue;
619                         }
620                         //erase a trailing slash if there is one
621                         if (suffixIs(tempBase, "/"))
622                                 tempBase.erase(tempBase.length() - 1, string::npos);
623
624                         string::size_type i = tempBase.length() - 1;
625                         while (i > 0 && tempBase[i] != '/')
626                                 --i;
627                         if (i > 0)
628                                 tempBase.erase(i, string::npos);
629                         else
630                                 tempBase = '/';
631                 } else if (temp.empty() && !rTemp.empty()) {
632                                 tempBase = os::current_root() + rTemp;
633                                 rTemp.erase();
634                 } else {
635                         // Add this piece to TempBase
636                         if (!suffixIs(tempBase, '/'))
637                                 tempBase += '/';
638                         tempBase += temp;
639                 }
640         }
641
642         // returns absolute path
643         return FileName(tempBase);
644 }
645
646
647 // Correctly append filename to the pathname.
648 // If pathname is '.', then don't use pathname.
649 // Chops any path of filename.
650 string const addName(string const & path, string const & fname)
651 {
652         string const basename = onlyFileName(fname);
653         string buf;
654
655         if (path != "." && path != "./" && !path.empty()) {
656                 buf = os::internal_path(path);
657                 if (!suffixIs(buf, '/'))
658                         buf += '/';
659         }
660
661         return buf + basename;
662 }
663
664
665 string const addPathName(std::string const & path, std::string const & fname)
666 {
667         string const pathpart = onlyPath(fname);
668         string const namepart = onlyFileName(fname);
669         string newpath = path;
670         if (!pathpart.empty())
671                 newpath = addPath(newpath, pathpart);
672         if (!namepart.empty())
673                 newpath = addName(newpath, namepart);
674         return newpath;
675 }
676
677
678 // Strips path from filename
679 string const onlyFileName(string const & fname)
680 {
681         if (fname.empty())
682                 return fname;
683
684         string::size_type j = fname.rfind('/');
685         if (j == string::npos) // no '/' in fname
686                 return fname;
687
688         // Strip to basename
689         return fname.substr(j + 1);
690 }
691
692
693 // Search the string for ${VAR} and $VAR and replace VAR using getenv.
694 // If VAR does not exist, ${VAR} and $VAR are left as is in the string.
695 string const replaceEnvironmentPath(string const & path)
696 {
697         if (!contains(path, '$'))
698                 return path;
699
700         // ${VAR} is defined as
701         // $\{[A-Za-z_][A-Za-z_0-9]*\}
702         static string const envvar_br = "[$]\\{([A-Za-z_][A-Za-z_0-9]*)\\}";
703
704         // $VAR is defined as:
705         // $[A-Za-z_][A-Za-z_0-9]*
706         static string const envvar = "[$]([A-Za-z_][A-Za-z_0-9]*)";
707
708         // Coverity thinks that the regex constructor can return an
709         // exception. We know that it is not true since our regex are
710         // hardcoded, but we have to protect against that nevertheless.
711         try {
712                 static regex const envvar_br_re("(.*)" + envvar_br + "(.*)");
713                 static regex const envvar_re("(.*)" + envvar + "(.*)");
714                 string result = path;
715                 while (1) {
716                         smatch what;
717                         bool brackets = true;
718                         if (!regex_match(result, what, envvar_br_re)) {
719                                 brackets = false;
720                                 if (!regex_match(result, what, envvar_re))
721                                         break;
722                         }
723                         string env_var = getEnv(what.str(2));
724                         if (env_var.empty()) {
725                                 // temporarily use alert/bell (0x07) in place of $
726                                 if (brackets)
727                                         env_var = "\a{" + what.str(2) + '}';
728                                 else
729                                         env_var = "\a" + what.str(2);
730                         }
731                         result = what.str(1) + env_var + what.str(3);
732                 }
733                 return subst(result, '\a', '$');
734         } catch (exception const & e) {
735                 LYXERR0("Something is very wrong: " << e.what());
736                 return path;
737         }
738 }
739
740
741 // Return a command prefix for setting the environment of the TeX engine.
742 string latexEnvCmdPrefix(string const & path, string const & lpath)
743 {
744         bool use_lpath = !(lpath.empty() || lpath == "." || lpath == "./");
745
746         if (path.empty() || (lyxrc.texinputs_prefix.empty() && !use_lpath))
747                 return string();
748
749         string texinputs_prefix = lyxrc.texinputs_prefix.empty() ? string()
750                 : os::latex_path_list(
751                         replaceCurdirPath(path, lyxrc.texinputs_prefix));
752         string const allother_prefix = os::latex_path_list(path);
753         string const sep = string(1, os::path_separator(os::TEXENGINE));
754         string const texinputs = getEnv("TEXINPUTS");
755         string const bibinputs = getEnv("BIBINPUTS");
756         string const bstinputs = getEnv("BSTINPUTS");
757         string const texfonts = getEnv("TEXFONTS");
758
759         if (use_lpath) {
760                 string const abslpath = FileName::isAbsolute(lpath)
761                         ? os::latex_path(lpath)
762                         : os::latex_path(FileName(path + "/" + lpath).realPath());
763                 if (texinputs_prefix.empty())
764                         texinputs_prefix = abslpath;
765                 else if (suffixIs(texinputs_prefix, sep))
766                         texinputs_prefix.append(abslpath + sep);
767                 else
768                         texinputs_prefix.append(sep + abslpath);
769         }
770
771         if (os::shell() == os::UNIX)
772                 return "env TEXINPUTS=\"." + sep + texinputs_prefix
773                                            + sep + texinputs + "\" "
774                          + "BIBINPUTS=\"." + sep + allother_prefix
775                                            + sep + bibinputs + "\" "
776                          + "BSTINPUTS=\"." + sep + allother_prefix
777                                            + sep + bstinputs + "\" "
778                          + "TEXFONTS=\"."  + sep + allother_prefix
779                                            + sep + texfonts + "\" ";
780         else
781                 // NOTE: the dummy blank dirs are necessary to force the
782                 //       QProcess parser to quote the argument (see bug 9453)
783                 return "cmd /d /c set \"TEXINPUTS=." + sep + " "
784                                                 + sep + texinputs_prefix
785                                                 + sep + texinputs + "\" & "
786                                + "set \"BIBINPUTS=." + sep + " "
787                                                 + sep + allother_prefix
788                                                 + sep + bibinputs + "\" & "
789                                + "set \"BSTINPUTS=." + sep + " "
790                                                 + sep + allother_prefix
791                                                 + sep + bstinputs + "\" & "
792                                + "set \"TEXFONTS=."  + sep + " "
793                                                 + sep + allother_prefix
794                                                 + sep + texfonts + "\" & ";
795 }
796
797
798 // Replace current directory in all elements of a path list with a given path.
799 string const replaceCurdirPath(string const & path, string const & pathlist)
800 {
801         string const oldpathlist = replaceEnvironmentPath(pathlist);
802         char const sep = os::path_separator();
803         string newpathlist;
804
805         for (size_t i = 0, k = 0; i != string::npos; k = i) {
806                 i = oldpathlist.find(sep, i);
807                 string p = oldpathlist.substr(k, i - k);
808                 if (FileName::isAbsolute(p)) {
809                         newpathlist += p;
810                 } else if (i > k) {
811                         size_t offset = 0;
812                         if (p == ".") {
813                                 offset = 1;
814                         } else if (prefixIs(p, "./")) {
815                                 offset = 2;
816                                 while (p[offset] == '/')
817                                         ++offset;
818                         }
819                         newpathlist += addPath(path, p.substr(offset));
820                         if (suffixIs(p, "//"))
821                                 newpathlist += '/';
822                 }
823                 if (i != string::npos) {
824                         newpathlist += sep;
825                         // Stop here if the last element is empty
826                         if (++i == oldpathlist.length())
827                                 break;
828                 }
829         }
830         return newpathlist;
831 }
832
833
834 // Make relative path out of two absolute paths
835 docstring const makeRelPath(docstring const & abspath, docstring const & basepath)
836 // Makes relative path out of absolute path. If it is deeper than basepath,
837 // it's easy. If basepath and abspath share something (they are all deeper
838 // than some directory), it'll be rendered using ..'s. If they are completely
839 // different, then the absolute path will be used as relative path.
840 {
841         docstring::size_type const abslen = abspath.length();
842         docstring::size_type const baselen = basepath.length();
843
844         docstring::size_type i = os::common_path(abspath, basepath);
845
846         if (i == 0) {
847                 // actually no match - cannot make it relative
848                 return abspath;
849         }
850
851         // Count how many dirs there are in basepath above match
852         // and append as many '..''s into relpath
853         docstring buf;
854         docstring::size_type j = i;
855         while (j < baselen) {
856                 if (basepath[j] == '/') {
857                         if (j + 1 == baselen)
858                                 break;
859                         buf += "../";
860                 }
861                 ++j;
862         }
863
864         // Append relative stuff from common directory to abspath
865         if (abspath[i] == '/')
866                 ++i;
867         for (; i < abslen; ++i)
868                 buf += abspath[i];
869         // Remove trailing /
870         if (suffixIs(buf, '/'))
871                 buf.erase(buf.length() - 1);
872         // Substitute empty with .
873         if (buf.empty())
874                 buf = '.';
875         return buf;
876 }
877
878
879 // Append sub-directory(ies) to a path in an intelligent way
880 string const addPath(string const & path, string const & path_2)
881 {
882         string buf;
883         string const path2 = os::internal_path(path_2);
884
885         if (!path.empty() && path != "." && path != "./") {
886                 buf = os::internal_path(path);
887                 if (path[path.length() - 1] != '/')
888                         buf += '/';
889         }
890
891         if (!path2.empty()) {
892                 string::size_type const p2start = path2.find_first_not_of('/');
893                 string::size_type const p2end = path2.find_last_not_of('/');
894                 string const tmp = path2.substr(p2start, p2end - p2start + 1);
895                 buf += tmp + '/';
896         }
897         return buf;
898 }
899
900
901 string const changeExtension(string const & oldname, string const & extension)
902 {
903         string::size_type const last_slash = oldname.rfind('/');
904         string::size_type last_dot = oldname.rfind('.');
905         if (last_dot < last_slash && last_slash != string::npos)
906                 last_dot = string::npos;
907
908         string ext;
909         // Make sure the extension starts with a dot
910         if (!extension.empty() && extension[0] != '.')
911                 ext= '.' + extension;
912         else
913                 ext = extension;
914
915         return os::internal_path(oldname.substr(0, last_dot) + ext);
916 }
917
918
919 string const removeExtension(string const & name)
920 {
921         return changeExtension(name, string());
922 }
923
924
925 string const addExtension(string const & name, string const & extension)
926 {
927         if (!extension.empty() && extension[0] != '.')
928                 return name + '.' + extension;
929         return name + extension;
930 }
931
932
933 /// Return the extension of the file (not including the .)
934 string const getExtension(string const & name)
935 {
936         string::size_type const last_slash = name.rfind('/');
937         string::size_type const last_dot = name.rfind('.');
938         if (last_dot != string::npos &&
939             (last_slash == string::npos || last_dot > last_slash))
940                 return name.substr(last_dot + 1,
941                                    name.length() - (last_dot + 1));
942         else
943                 return string();
944 }
945
946
947 docstring const provideScheme(docstring const & name, docstring const & scheme)
948 {
949         if (prefixIs(name, scheme + "://"))
950                 return name;
951         QUrl url(toqstr(name));
952         if (!url.scheme().isEmpty())
953                 // Has a scheme. Return as is.
954                 return name;
955         if (scheme == from_ascii("doi")) {
956                 // check if it is the pure DOI (without URL)
957                 if (isDigitASCII(name[1]))
958                         return from_ascii("https://doi.org/") + name;
959         }
960         url.setScheme(toqstr(scheme));
961         return qstring_to_ucs4(url.toString());
962 }
963
964
965 string const unzippedFileName(string const & zipped_file)
966 {
967         string const ext = getExtension(zipped_file);
968         if (ext == "gz" || ext == "z" || ext == "Z")
969                 return changeExtension(zipped_file, string());
970         else if (ext == "svgz")
971                 return changeExtension(zipped_file, "svg");
972         return onlyPath(zipped_file) + "unzipped_" + onlyFileName(zipped_file);
973 }
974
975
976 FileName const unzipFile(FileName const & zipped_file, string const & unzipped_file)
977 {
978         FileName const tempfile = FileName(unzipped_file.empty() ?
979                 unzippedFileName(zipped_file.toFilesystemEncoding()) :
980                 unzipped_file);
981         // Run gunzip
982         string const command = "gunzip -c \"" +
983                 zipped_file.toFilesystemEncoding() + "\" > \"" +
984                 tempfile.toFilesystemEncoding() + "\"";
985         Systemcall one;
986         one.startscript(Systemcall::Wait, command);
987         // test that command was executed successfully (anon)
988         // yes, please do. (Lgb)
989         return tempfile;
990 }
991
992
993 docstring const makeDisplayPath(string const & path, unsigned int threshold)
994 {
995         string str = path;
996
997         // Recode URL encoded chars.
998         str = from_percent_encoding(str);
999
1000         // If file is from LyXDir, display it as if it were relative.
1001         string const system = package().system_support().absFileName();
1002         if (prefixIs(str, system) && str != system)
1003                 return from_utf8("[" + str.erase(0, system.length()) + "]");
1004
1005         // replace /home/blah with ~/
1006         string const home = Package::get_home_dir().absFileName();
1007         if (!home.empty() && prefixIs(str, home))
1008                 str = subst(str, home, "~");
1009
1010         if (str.length() <= threshold)
1011                 return from_utf8(os::external_path(str));
1012
1013         string const prefix = ".../";
1014         docstring dstr = from_utf8(str);
1015         docstring temp;
1016
1017         while (dstr.length() > threshold)
1018                 dstr = split(dstr, temp, '/');
1019
1020         // Did we shorten everything away?
1021         if (dstr.empty()) {
1022                 // Yes, filename itself is too long.
1023                 // Pick the start and the end of the filename.
1024                 docstring fstr = from_utf8(onlyFileName(path));
1025                 dstr = fstr;
1026                 if (support::truncateWithEllipsis(dstr, threshold / 2))
1027                         dstr += fstr.substr(fstr.length() - threshold / 2 - 2,
1028                                                                 docstring::npos);
1029         }
1030
1031         return from_utf8(os::external_path(prefix + to_utf8(dstr)));
1032 }
1033
1034
1035 #ifdef HAVE_READLINK
1036 bool readLink(FileName const & file, FileName & link)
1037 {
1038         string const encoded = file.toFilesystemEncoding();
1039 #ifdef HAVE_DEF_PATH_MAX
1040         char linkbuffer[PATH_MAX + 1];
1041         ssize_t const nRead = ::readlink(encoded.c_str(),
1042                                      linkbuffer, sizeof(linkbuffer) - 1);
1043         if (nRead <= 0)
1044                 return false;
1045         linkbuffer[nRead] = '\0'; // terminator
1046 #else
1047         vector<char> buf(1024);
1048         int nRead = -1;
1049
1050         while (true) {
1051                 nRead = ::readlink(encoded.c_str(), &buf[0], buf.size() - 1);
1052                 if (nRead < 0) {
1053                         return false;
1054                 }
1055                 if (static_cast<size_t>(nRead) < buf.size() - 1) {
1056                         break;
1057                 }
1058                 buf.resize(buf.size() * 2);
1059         }
1060         buf[nRead] = '\0'; // terminator
1061         const char * linkbuffer = &buf[0];
1062 #endif
1063         link = makeAbsPath(linkbuffer, onlyPath(file.absFileName()));
1064         return true;
1065 }
1066 #else
1067 bool readLink(FileName const &, FileName &)
1068 {
1069         return false;
1070 }
1071 #endif
1072
1073
1074 cmd_ret const runCommand(string const & cmd)
1075 {
1076         // FIXME: replace all calls to RunCommand with ForkedCall
1077         // (if the output is not needed) or the code in ISpell.cpp
1078         // (if the output is needed).
1079
1080         // One question is if we should use popen or
1081         // create our own popen based on fork, exec, pipe
1082         // of course the best would be to have a
1083         // pstream (process stream), with the
1084         // variants ipstream, opstream
1085
1086         if (verbose)
1087                 lyxerr << "\nRunning: " << cmd << endl;
1088         else
1089                 LYXERR(Debug::INFO,"Running: " << cmd);
1090
1091 #if defined (_WIN32)
1092         STARTUPINFO startup;
1093         PROCESS_INFORMATION process;
1094         SECURITY_ATTRIBUTES security;
1095         HANDLE in, out;
1096         FILE * inf = 0;
1097         bool err2out = false;
1098         string command;
1099         string const infile = trim(split(cmd, command, '<'), " \"");
1100         command = rtrim(command);
1101         if (suffixIs(command, "2>&1")) {
1102                 command = rtrim(command, "2>&1");
1103                 err2out = true;
1104         }
1105         string const cmdarg = "/d /c \"" + command + "\"";
1106         string const comspec = getEnv("COMSPEC");
1107
1108         security.nLength = sizeof(SECURITY_ATTRIBUTES);
1109         security.bInheritHandle = TRUE;
1110         security.lpSecurityDescriptor = NULL;
1111
1112         if (CreatePipe(&in, &out, &security, 0)) {
1113                 memset(&startup, 0, sizeof(STARTUPINFO));
1114                 memset(&process, 0, sizeof(PROCESS_INFORMATION));
1115
1116                 startup.cb = sizeof(STARTUPINFO);
1117                 startup.dwFlags = STARTF_USESTDHANDLES;
1118
1119                 startup.hStdError = err2out ? out : GetStdHandle(STD_ERROR_HANDLE);
1120                 startup.hStdInput = infile.empty()
1121                         ? GetStdHandle(STD_INPUT_HANDLE)
1122                         : CreateFile(infile.c_str(), GENERIC_READ,
1123                                 FILE_SHARE_READ, &security, OPEN_EXISTING,
1124                                 FILE_ATTRIBUTE_NORMAL, NULL);
1125                 startup.hStdOutput = out;
1126
1127                 if (startup.hStdInput != INVALID_HANDLE_VALUE &&
1128                         CreateProcess(comspec.c_str(), (LPTSTR)cmdarg.c_str(),
1129                                 &security, &security, TRUE, CREATE_NO_WINDOW,
1130                                 0, 0, &startup, &process)) {
1131
1132                         CloseHandle(process.hThread);
1133                         int fno = _open_osfhandle((intptr_t)in, _O_RDONLY);
1134                         CloseHandle(out);
1135                         inf = _fdopen(fno, "r");
1136                 }
1137         }
1138 #elif defined (HAVE_POPEN)
1139         FILE * inf = ::popen(cmd.c_str(), os::popen_read_mode());
1140 #elif defined (HAVE__POPEN)
1141         FILE * inf = ::_popen(cmd.c_str(), os::popen_read_mode());
1142 #else
1143 #error No popen() function.
1144 #endif
1145
1146         // (Claus Hentschel) Check if popen was successful ;-)
1147         if (!inf) {
1148                 lyxerr << "RunCommand: could not start child process" << endl;
1149                 return { false, string() };
1150         }
1151
1152         string result;
1153         int c = fgetc(inf);
1154         while (c != EOF) {
1155                 result += static_cast<char>(c);
1156                 c = fgetc(inf);
1157         }
1158
1159 #if defined (_WIN32)
1160         WaitForSingleObject(process.hProcess, INFINITE);
1161         DWORD pret;
1162         BOOL success = GetExitCodeProcess(process.hProcess, &pret);
1163         bool valid = (pret == 0) && success;
1164         if (!success)
1165                 pret = -1;
1166         if (!infile.empty())
1167                 CloseHandle(startup.hStdInput);
1168         CloseHandle(process.hProcess);
1169         if (fclose(inf) != 0)
1170                 pret = -1;
1171 #elif defined (HAVE_PCLOSE)
1172         int const pret = pclose(inf);
1173         bool const valid = (WEXITSTATUS(pret) == 0);
1174 #elif defined (HAVE__PCLOSE)
1175         int const pret = _pclose(inf);
1176         bool const valid = (WEXITSTATUS(pret) == 0);
1177 #else
1178 #error No pclose() function.
1179 #endif
1180
1181         if (pret == -1)
1182                 perror("RunCommand: could not terminate child process");
1183
1184         return { valid, result };
1185 }
1186
1187
1188 FileName const findtexfile(string const & fil, string const & /*format*/,
1189                                                    bool const onlykpse)
1190 {
1191         /* There is no problem to extend this function to use other
1192            methods to look for files. It could be setup to look
1193            in environment paths and also if wanted as a last resort
1194            to a recursive find. One of the easier extensions would
1195            perhaps be to use the LyX file lookup methods. But! I am
1196            going to implement this until I see some demand for it.
1197            Lgb
1198         */
1199
1200         // If the file can be found directly, we just return a
1201         // absolute path version of it.
1202         if (!onlykpse) {
1203                 FileName const absfile(makeAbsPath(fil));
1204                 if (absfile.exists())
1205                         return absfile;
1206         }
1207
1208         // Now we try to find it using kpsewhich.
1209         // It seems from the kpsewhich manual page that it is safe to use
1210         // kpsewhich without --format: "When the --format option is not
1211         // given, the search path used when looking for a file is inferred
1212         // from the name given, by looking for a known extension. If no
1213         // known extension is found, the search path for TeX source files
1214         // is used."
1215         // However, we want to take advantage of the format sine almost all
1216         // the different formats has environment variables that can be used
1217         // to control which paths to search. f.ex. bib looks in
1218         // BIBINPUTS and TEXBIB. Small list follows:
1219         // bib - BIBINPUTS, TEXBIB
1220         // bst - BSTINPUTS
1221         // graphic/figure - TEXPICTS, TEXINPUTS
1222         // ist - TEXINDEXSTYLE, INDEXSTYLE
1223         // pk - PROGRAMFONTS, PKFONTS, TEXPKS, GLYPHFONTS, TEXFONTS
1224         // tex - TEXINPUTS
1225         // tfm - TFMFONTS, TEXFONTS
1226         // This means that to use kpsewhich in the best possible way we
1227         // should help it by setting additional path in the approp. envir.var.
1228         string const kpsecmd = "kpsewhich " + fil;
1229
1230         cmd_ret const c = runCommand(kpsecmd);
1231
1232         LYXERR(Debug::OUTFILE, "kpse status = " << c.valid << '\n'
1233                                                 << "kpse result = `" << rtrim(c.result, "\n\r") << '\'');
1234         if (c.valid)
1235                 return FileName(rtrim(to_utf8(from_filesystem8bit(c.result)), "\n\r"));
1236         else
1237                 return FileName();
1238 }
1239
1240
1241 int compare_timestamps(FileName const & file1, FileName const & file2)
1242 {
1243         // If the original is newer than the copy, then copy the original
1244         // to the new directory.
1245
1246         int cmp = 0;
1247         if (file1.exists() && file2.exists()) {
1248                 double const tmp = difftime(file1.lastModified(), file2.lastModified());
1249                 if (tmp != 0)
1250                         cmp = tmp > 0 ? 1 : -1;
1251
1252         } else if (file1.exists()) {
1253                 cmp = 1;
1254         } else if (file2.exists()) {
1255                 cmp = -1;
1256         }
1257
1258         return cmp;
1259 }
1260
1261
1262 bool prefs2prefs(FileName const & filename, FileName const & tempfile, bool lfuns)
1263 {
1264         FileName const script = libFileSearch("scripts", "prefs2prefs.py");
1265         if (script.empty()) {
1266                 LYXERR0("Could not find bind file conversion "
1267                                 "script prefs2prefs.py.");
1268                 return false;
1269         }
1270
1271         ostringstream command;
1272         command << os::python() << ' ' << quoteName(script.toFilesystemEncoding())
1273           << ' ' << (lfuns ? "-l" : "-p") << ' '
1274                 << quoteName(filename.toFilesystemEncoding())
1275                 << ' ' << quoteName(tempfile.toFilesystemEncoding());
1276         string const command_str = command.str();
1277
1278         LYXERR(Debug::FILES, "Running `" << command_str << '\'');
1279
1280         cmd_ret const ret = runCommand(command_str);
1281         if (!ret.valid) {
1282                 LYXERR0("Could not run file conversion script prefs2prefs.py.");
1283                 return false;
1284         }
1285         return true;
1286 }
1287
1288
1289 bool configFileNeedsUpdate(string const & file)
1290 {
1291         // We cannot initialize configure_script directly because the package
1292         // is not initialized yet when static objects are constructed.
1293         static FileName configure_script;
1294         static bool firstrun = true;
1295         if (firstrun) {
1296                 configure_script =
1297                         FileName(addName(package().system_support().absFileName(),
1298                                 "configure.py"));
1299                 firstrun = false;
1300         }
1301
1302         FileName absfile =
1303                 FileName(addName(package().user_support().absFileName(), file));
1304         return !absfile.exists()
1305                 || configure_script.lastModified() > absfile.lastModified();
1306 }
1307
1308
1309 int fileLock(const char * lock_file)
1310 {
1311         int fd = -1;
1312 #if defined(HAVE_LOCKF)
1313         fd = open(lock_file, O_CREAT|O_APPEND|O_SYNC|O_RDWR, 0666);
1314         if (fd == -1)
1315                 return -1;
1316         if (lockf(fd, F_LOCK, 0) != 0) {
1317                 close(fd);
1318                 return -1;
1319         }
1320 #endif
1321         return fd;
1322 }
1323
1324
1325 void fileUnlock(int fd, const char * /* lock_file*/)
1326 {
1327 #if defined(HAVE_LOCKF)
1328         if (fd >= 0) {
1329                 if (lockf(fd, F_ULOCK, 0))
1330                         LYXERR0("Can't unlock the file.");
1331                 close(fd);
1332         }
1333 #endif
1334 }
1335
1336
1337 std::string toHexHash(const std::string & str, bool shorten)
1338 {
1339         auto hashAlgo = QCryptographicHash::Sha256;
1340
1341         QByteArray hash = QCryptographicHash::hash(toqstr(str).toLocal8Bit(), hashAlgo);
1342         QString qshash=QString(hash.toHex());
1343
1344         /* For shortened case we take 12 leftmost chars (6 bytes encoded).
1345          * Random experiment shows:
1346          *  8 chars: 16 collisions for 10^5 graphic filenames
1347          * 12 chars:  0 collisions for 10^5 graphic filenames
1348          */
1349         if (shorten)
1350                 qshash=qshash.left(12);
1351
1352         return fromqstr(qshash);
1353 }
1354
1355
1356 std::string sanitizeFileName(const std::string & str)
1357 {
1358         // The list of characters to keep is probably over-restrictive,
1359         // but it is not really a problem.
1360         // Apart from non-ASCII characters, at least the following characters
1361         // are forbidden: '/', '.', ' ', and ':'.
1362         // On windows it is not possible to create files with '<', '>' or '?'
1363         // in the name.
1364         static std::string const keep = "abcdefghijklmnopqrstuvwxyz"
1365                                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1366                                    "+-0123456789;=";
1367
1368         std::string name = str;
1369         string::size_type pos = 0;
1370         while ((pos = name.find_first_not_of(keep, pos)) != string::npos)
1371                 name[pos++] = '_';
1372
1373         return name;
1374 }
1375
1376 } // namespace support
1377 } // namespace lyx