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