]> git.lyx.org Git - lyx.git/blob - src/support/FileName.cpp
GuiCharacter: Add menu to Restore button
[lyx.git] / src / support / FileName.cpp
1 /**
2  * \file FileName.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Angus Leeming
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "support/FileName.h"
14 #include "support/FileNameList.h"
15
16 #include "support/debug.h"
17 #include "support/filetools.h"
18 #include "support/lassert.h"
19 #include "support/lstrings.h"
20 #include "support/mutex.h"
21 #include "support/os.h"
22 #include "support/Package.h"
23 #include "support/qstring_helpers.h"
24
25 #include <QDateTime>
26 #include <QDir>
27 #include <QFile>
28 #include <QFileInfo>
29 #include <QList>
30 #include <QTemporaryFile>
31 #include <QTime>
32
33 #include <boost/crc.hpp>
34
35 #include <algorithm>
36 #include <iterator>
37 #include <fstream>
38 #include <iomanip>
39 #include <map>
40 #include <sstream>
41
42 #ifdef HAVE_SYS_TYPES_H
43 # include <sys/types.h>
44 #endif
45 #ifdef HAVE_SYS_STAT_H
46 # include <sys/stat.h>
47 #endif
48 #ifdef HAVE_UNISTD_H
49 # include <unistd.h>
50 #endif
51 #ifdef HAVE_DIRECT_H
52 # include <direct.h>
53 #endif
54 #ifdef _WIN32
55 # include <windows.h>
56 #endif
57
58 #include <cerrno>
59 #include <fcntl.h>
60
61 // Three implementations of checksum(), depending on having mmap support or not.
62 #if defined(HAVE_MMAP) && defined(HAVE_MUNMAP)
63 #define SUM_WITH_MMAP
64 #include <sys/mman.h>
65 #endif // SUM_WITH_MMAP
66
67 using namespace std;
68 using namespace lyx::support;
69
70 namespace lyx {
71 namespace support {
72
73 /////////////////////////////////////////////////////////////////////
74 //
75 // FileName::Private
76 //
77 /////////////////////////////////////////////////////////////////////
78
79 struct FileName::Private
80 {
81         Private() {}
82
83         Private(string const & abs_filename) : fi(toqstr(handleTildeName(abs_filename)))
84         {
85                 name = fromqstr(fi.absoluteFilePath());
86                 fi.setCaching(fi.exists() ? true : false);
87         }
88         ///
89         inline void refresh()
90         {
91                 fi.refresh();
92         }
93
94         static
95         bool isFilesystemEqual(QString const & lhs, QString const & rhs)
96         {
97                 return QString::compare(lhs, rhs, os::isFilesystemCaseSensitive() ?
98                         Qt::CaseSensitive : Qt::CaseInsensitive) == 0;
99         }
100
101         static
102         string const handleTildeName(string const & name)
103         {
104                 return name == "~" ? Package::get_home_dir().absFileName() :
105                         prefixIs(name, "~/") ? Package::get_home_dir().absFileName() + name.substr(1) :
106                         name;
107         }
108
109         /// The absolute file name in UTF-8 encoding.
110         std::string name;
111         ///
112         QFileInfo fi;
113 };
114
115 /////////////////////////////////////////////////////////////////////
116 //
117 // FileName
118 //
119 /////////////////////////////////////////////////////////////////////
120
121
122 FileName::FileName() : d(new Private)
123 {
124 }
125
126
127 FileName::FileName(string const & abs_filename)
128         : d(abs_filename.empty() ? new Private : new Private(abs_filename))
129 {
130         //LYXERR(Debug::FILES, "FileName(" << abs_filename << ')');
131         LATTEST(empty() || isAbsolute(d->name));
132 }
133
134
135 FileName::~FileName()
136 {
137         delete d;
138 }
139
140
141 FileName::FileName(FileName const & rhs) : d(new Private)
142 {
143         d->name = rhs.d->name;
144         d->fi = rhs.d->fi;
145 }
146
147
148 FileName::FileName(FileName const & rhs, string const & suffix) : d(new Private)
149 {
150         set(rhs, suffix);
151 }
152
153
154 FileName & FileName::operator=(FileName const & rhs)
155 {
156         if (&rhs == this)
157                 return *this;
158         d->name = rhs.d->name;
159         d->fi = rhs.d->fi;
160         return *this;
161 }
162
163
164 bool FileName::empty() const
165 {
166         return d->name.empty();
167 }
168
169
170 bool FileName::isAbsolute(string const & name)
171 {
172         QFileInfo fi(toqstr(Private::handleTildeName(name)));
173         return fi.isAbsolute();
174 }
175
176
177 string FileName::absFileName() const
178 {
179         return d->name;
180 }
181
182
183 string FileName::realPath() const
184 {
185         return os::real_path(absFileName());
186 }
187
188
189 void FileName::set(string const & name)
190 {
191         d->fi.setFile(toqstr(Private::handleTildeName(name)));
192         d->name = fromqstr(d->fi.absoluteFilePath());
193         //LYXERR(Debug::FILES, "FileName::set(" << name << ')');
194         LATTEST(empty() || isAbsolute(d->name));
195 }
196
197
198 void FileName::set(FileName const & rhs, string const & suffix)
199 {
200         if (!rhs.d->fi.isDir())
201                 d->fi.setFile(rhs.d->fi.filePath() + toqstr(suffix));
202         else
203                 d->fi.setFile(QDir(rhs.d->fi.absoluteFilePath()), toqstr(suffix));
204         d->name = fromqstr(d->fi.absoluteFilePath());
205         //LYXERR(Debug::FILES, "FileName::set(" << d->name << ')');
206         LATTEST(empty() || isAbsolute(d->name));
207 }
208
209
210 void FileName::erase()
211 {
212         d->name.clear();
213         d->fi = QFileInfo();
214 }
215
216
217 bool FileName::copyTo(FileName const & name, bool keepsymlink) const
218 {
219         FileNameSet visited;
220         return copyTo(name, keepsymlink, visited);
221 }
222
223
224 bool FileName::copyTo(FileName const & name, bool keepsymlink,
225                       FileName::FileNameSet & visited) const
226 {
227         LYXERR(Debug::FILES, "Copying " << name << " keep symlink: " << keepsymlink);
228         if (keepsymlink && name.isSymLink()) {
229                 visited.insert(*this);
230                 FileName const target(fromqstr(name.d->fi.symLinkTarget()));
231                 if (visited.find(target) != visited.end()) {
232                         LYXERR(Debug::FILES, "Found circular symlink: " << target);
233                         return false;
234                 }
235                 return copyTo(target, true);
236         }
237         QFile::remove(name.d->fi.absoluteFilePath());
238         bool success = QFile::copy(d->fi.absoluteFilePath(), name.d->fi.absoluteFilePath());
239         if (!success)
240                 LYXERR0("FileName::copyTo(): Could not copy file "
241                         << *this << " to " << name);
242         return success;
243 }
244
245
246 bool FileName::renameTo(FileName const & name) const
247 {
248         LYXERR(Debug::FILES, "Renaming " << name << " as " << *this);
249         bool success = QFile::rename(d->fi.absoluteFilePath(), name.d->fi.absoluteFilePath());
250         if (!success)
251                 LYXERR0("Could not rename file " << *this << " to " << name);
252         return success;
253 }
254
255
256 bool FileName::moveTo(FileName const & name) const
257 {
258         LYXERR(Debug::FILES, "Moving " << *this << " to " << name);
259         QFile::remove(name.d->fi.absoluteFilePath());
260
261         bool success = QFile::rename(d->fi.absoluteFilePath(),
262                 name.d->fi.absoluteFilePath());
263         if (!success)
264                 LYXERR0("Could not move file " << *this << " to " << name);
265         return success;
266 }
267
268
269 bool FileName::changePermission(unsigned long int mode) const
270 {
271 #if defined (HAVE_CHMOD) && defined (HAVE_MODE_T)
272         if (::chmod(toFilesystemEncoding().c_str(), mode_t(mode)) != 0) {
273                 LYXERR0("File " << *this << ": cannot change permission to "
274                         << mode << ".");
275                 return false;
276         }
277 #endif
278         return true;
279 }
280
281 bool FileName::clonePermissions(FileName const & source)
282 {
283         QFile fin(toqstr(source.absFileName()));
284         QFile f(toqstr(absFileName()));
285
286         return f.setPermissions(fin.permissions());
287 }
288
289 string FileName::toFilesystemEncoding() const
290 {
291         // This doesn't work on Windows for non ascii file names.
292         QByteArray const encoded = QFile::encodeName(d->fi.absoluteFilePath());
293         return string(encoded.begin(), encoded.end());
294 }
295
296
297 string FileName::toSafeFilesystemEncoding(os::file_access how) const
298 {
299         // This will work on Windows for non ascii file names.
300         QString const safe_path =
301                 toqstr(os::safe_internal_path(absFileName(), how));
302         QByteArray const encoded = QFile::encodeName(safe_path);
303         return string(encoded.begin(), encoded.end());
304 }
305
306
307 FileName FileName::fromFilesystemEncoding(string const & name)
308 {
309         QByteArray const encoded(name.c_str(), name.length());
310         return FileName(fromqstr(QFile::decodeName(encoded)));
311 }
312
313
314 bool FileName::exists() const
315 {
316         return !empty() && d->fi.exists();
317 }
318
319
320 bool FileName::isSymLink() const
321 {
322         return !empty() && d->fi.isSymLink();
323 }
324
325
326 //QFileInfo caching info might fool this test if file was changed meanwhile.
327 //refresh() helps, but we don't want to put it blindly here, because it might
328 //trigger slowdown on networked file systems.
329 bool FileName::isFileEmpty() const
330 {
331         LASSERT(!empty(), return true);
332         return d->fi.size() == 0;
333 }
334
335
336 bool FileName::isDirectory() const
337 {
338         return !empty() && d->fi.isDir();
339 }
340
341
342 bool FileName::isReadOnly() const
343 {
344         LASSERT(!empty(), return true);
345         return d->fi.isReadable() && !d->fi.isWritable();
346 }
347
348
349 bool FileName::isReadableDirectory() const
350 {
351         return isDirectory() && d->fi.isReadable();
352 }
353
354
355 string FileName::onlyFileName() const
356 {
357         return fromqstr(d->fi.fileName());
358 }
359
360
361 string FileName::onlyFileNameWithoutExt() const
362 {
363         return fromqstr(d->fi.completeBaseName());
364 }
365
366
367 string FileName::extension() const
368 {
369         return fromqstr(d->fi.suffix());
370 }
371
372
373 bool FileName::hasExtension(const string & ext)
374 {
375         return Private::isFilesystemEqual(d->fi.suffix(), toqstr(ext));
376 }
377
378
379 FileName FileName::onlyPath() const
380 {
381         FileName path;
382         if (empty())
383                 return path;
384         path.d->fi.setFile(d->fi.path());
385         path.d->name = fromqstr(path.d->fi.absoluteFilePath());
386         return path;
387 }
388
389
390 FileName FileName::parentPath() const
391 {
392         FileName path;
393         // return empty path for parent of root dir
394         // parent of empty path is empty too
395         if (empty() || d->fi.isRoot())
396                 return path;
397         path.d->fi.setFile(d->fi.path());
398         path.d->name = fromqstr(path.d->fi.absoluteFilePath());
399         return path;
400 }
401
402
403 bool FileName::isReadableFile() const
404 {
405         return !empty() && d->fi.isFile() && d->fi.isReadable();
406 }
407
408
409 bool FileName::isWritable() const
410 {
411         return !empty() && d->fi.isWritable();
412 }
413
414
415 bool FileName::isDirWritable() const
416 {
417         LASSERT(isDirectory(), return false);
418         QFileInfo tmp(QDir(d->fi.absoluteFilePath()), "lyxwritetest");
419         QTemporaryFile qt_tmp(tmp.absoluteFilePath());
420         if (qt_tmp.open()) {
421                 LYXERR(Debug::FILES, "Directory " << *this << " is writable");
422                 return true;
423         }
424         LYXERR(Debug::FILES, "Directory " << *this << " is not writable");
425         return false;
426 }
427
428
429 FileNameList FileName::dirList(string const & ext) const
430 {
431         FileNameList dirlist;
432         if (!isDirectory()) {
433                 LYXERR0("Directory '" << *this << "' does not exist!");
434                 return dirlist;
435         }
436
437         // If the directory is specified without a trailing '/', absoluteDir()
438         // would return the parent dir, so we must use absoluteFilePath() here.
439         QDir dir = d->fi.absoluteFilePath();
440
441         if (!ext.empty()) {
442                 QString filter;
443                 switch (ext[0]) {
444                 case '.': filter = "*" + toqstr(ext); break;
445                 case '*': filter = toqstr(ext); break;
446                 default: filter = "*." + toqstr(ext);
447                 }
448                 dir.setNameFilters(QStringList(filter));
449                 LYXERR(Debug::FILES, "filtering on extension "
450                         << fromqstr(filter) << " is requested.");
451         }
452
453         QFileInfoList list = dir.entryInfoList();
454         for (int i = 0; i != list.size(); ++i) {
455                 FileName fi(fromqstr(list.at(i).absoluteFilePath()));
456                 dirlist.push_back(fi);
457                 LYXERR(Debug::FILES, "found file " << fi);
458         }
459
460         return dirlist;
461 }
462
463
464 FileName FileName::getcwd()
465 {
466         // return makeAbsPath("."); would create an infinite loop
467         QFileInfo fi(".");
468         return FileName(fromqstr(fi.absoluteFilePath()));
469 }
470
471
472 FileName FileName::tempPath()
473 {
474         return FileName(os::internal_path(fromqstr(QDir::tempPath())));
475 }
476
477
478 void FileName::refresh() const
479 {
480         d->refresh();
481 }
482
483
484 time_t FileName::lastModified() const
485 {
486         // QFileInfo caches information about the file. So, in case this file has
487         // been touched between the object creation and now, we refresh the file
488         // information.
489         d->refresh();
490         return d->fi.lastModified().toTime_t();
491 }
492
493
494 bool FileName::chdir() const
495 {
496         return QDir::setCurrent(d->fi.absoluteFilePath());
497 }
498
499
500 bool FileName::link(FileName const & name) const
501 {
502         return QFile::link(toqstr(absFileName()), toqstr(name.absFileName()));
503 }
504
505
506 unsigned long checksum_ifstream_fallback(char const * file)
507 {
508         unsigned long result = 0;
509         //LYXERR(Debug::FILES, "lyx::sum() using istreambuf_iterator (fast)");
510         ifstream ifs(file, ios_base::in | ios_base::binary);
511         if (!ifs)
512                 return result;
513
514         istreambuf_iterator<char> beg(ifs);
515         istreambuf_iterator<char> end;
516         boost::crc_32_type crc;
517         crc = for_each(beg, end, crc);
518         result = crc.checksum();
519         return result;
520 }
521
522 unsigned long FileName::checksum() const
523 {
524         unsigned long result = 0;
525
526         if (!exists()) {
527                 //LYXERR0("File \"" << absFileName() << "\" does not exist!");
528                 return result;
529         }
530         // a directory may be passed here so we need to test it. (bug 3622)
531         if (isDirectory()) {
532                 LYXERR0('"' << absFileName() << "\" is a directory!");
533                 return result;
534         }
535
536         // This is used in the debug output at the end of the method.
537         static QTime t;
538         if (lyxerr.debugging(Debug::FILES))
539                 t.restart();
540
541 #if QT_VERSION >= 0x999999
542         // First version of checksum uses Qt4.4 mmap support.
543         // FIXME: This code is not ready with Qt4.4.2,
544         // see http://www.lyx.org/trac/ticket/5293
545         // FIXME: should we check if the MapExtension extension is supported?
546         // see QAbstractFileEngine::supportsExtension() and
547         // QAbstractFileEngine::MapExtension)
548         QFile qf(fi.filePath());
549         if (!qf.open(QIODevice::ReadOnly))
550                 return result;
551         qint64 size = fi.size();
552         uchar * ubeg = qf.map(0, size);
553         uchar * uend = ubeg + size;
554         boost::crc_32_type ucrc;
555         ucrc.process_block(ubeg, uend);
556         qf.unmap(ubeg);
557         qf.close();
558         result = ucrc.checksum();
559
560 #else // QT_VERSION
561
562         string const encoded = toSafeFilesystemEncoding();
563         char const * file = encoded.c_str();
564
565  #ifdef SUM_WITH_MMAP
566         //LYXERR(Debug::FILES, "using mmap (lightning fast)");
567
568         int fd = open(file, O_RDONLY);
569         if (!fd)
570                 return result;
571
572         struct stat info;
573         if (fstat(fd, &info)){
574                 // fstat fails on samba shares (bug 5891)
575                 close(fd);
576                 return checksum_ifstream_fallback(file);
577         }
578
579         void * mm = mmap(0, info.st_size, PROT_READ,
580                          MAP_PRIVATE, fd, 0);
581         // Some platforms have the wrong type for MAP_FAILED (compaq cxx).
582         if (mm == reinterpret_cast<void*>(MAP_FAILED)) {
583                 close(fd);
584                 return result;
585         }
586
587         char * beg = static_cast<char*>(mm);
588         char * end = beg + info.st_size;
589
590         boost::crc_32_type crc;
591         crc.process_block(beg, end);
592         result = crc.checksum();
593
594         munmap(mm, info.st_size);
595         close(fd);
596
597  #else // no SUM_WITH_MMAP
598         result = checksum_ifstream_fallback(file);
599  #endif // SUM_WITH_MMAP
600 #endif // QT_VERSION
601
602         LYXERR(Debug::FILES, "Checksumming \"" << absFileName() << "\" "
603                 << result << " lasted " << t.elapsed() << " ms.");
604         return result;
605 }
606
607
608 bool FileName::removeFile() const
609 {
610         bool const success = QFile::remove(d->fi.absoluteFilePath());
611         d->refresh();
612         if (!success && exists())
613                 LYXERR0("Could not delete file " << *this);
614         return success;
615 }
616
617
618 static bool rmdir(QFileInfo const & fi)
619 {
620         QDir dir(fi.absoluteFilePath());
621         QFileInfoList list = dir.entryInfoList();
622         bool success = true;
623         for (int i = 0; i != list.size(); ++i) {
624                 if (list.at(i).fileName() == ".")
625                         continue;
626                 if (list.at(i).fileName() == "..")
627                         continue;
628                 bool removed;
629                 if (list.at(i).isDir()) {
630                         LYXERR(Debug::FILES, "Removing dir "
631                                 << fromqstr(list.at(i).absoluteFilePath()));
632                         removed = rmdir(list.at(i));
633                 }
634                 else {
635                         LYXERR(Debug::FILES, "Removing file "
636                                 << fromqstr(list.at(i).absoluteFilePath()));
637                         removed = dir.remove(list.at(i).fileName());
638                 }
639                 if (!removed) {
640                         success = false;
641                         LYXERR0("Could not delete "
642                                 << fromqstr(list.at(i).absoluteFilePath()));
643                 }
644         }
645         QDir parent = fi.absolutePath();
646         success &= parent.rmdir(fi.fileName());
647         return success;
648 }
649
650
651 bool FileName::destroyDirectory() const
652 {
653         bool const success = rmdir(d->fi);
654         if (!success)
655                 LYXERR0("Could not delete " << *this);
656
657         return success;
658 }
659
660
661 // Only used in non Win32 platforms
662 static int mymkdir(char const * pathname, unsigned long int mode)
663 {
664         // FIXME: why don't we have mode_t in lyx::mkdir prototype ??
665 #if HAVE_MKDIR
666 # if MKDIR_TAKES_ONE_ARG
667         // MinGW32
668         return ::mkdir(pathname);
669         // FIXME: "Permissions of created directories are ignored on this system."
670 # else
671         // POSIX
672         return ::mkdir(pathname, mode_t(mode));
673 # endif
674 #elif defined(_WIN32)
675         // plain Windows 32
676         return CreateDirectory(pathname, 0) != 0 ? 0 : -1;
677         // FIXME: "Permissions of created directories are ignored on this system."
678 #elif HAVE__MKDIR
679         return ::_mkdir(pathname);
680         // FIXME: "Permissions of created directories are ignored on this system."
681 #else
682 #   error "Don't know how to create a directory on this system."
683 #endif
684
685 }
686
687
688 bool FileName::createDirectory(int permission) const
689 {
690         LASSERT(!empty(), return false);
691 #ifdef Q_OS_WIN32
692         // FIXME: "Permissions of created directories are ignored on this system."
693         return createPath();
694 #else
695         return mymkdir(toFilesystemEncoding().c_str(), permission) == 0;
696 #endif
697 }
698
699
700 bool FileName::createPath() const
701 {
702         LASSERT(!empty(), return false);
703         LYXERR(Debug::FILES, "creating path '" << *this << "'.");
704         if (isDirectory())
705                 return false;
706
707         QDir dir;
708         bool success = dir.mkpath(d->fi.absoluteFilePath());
709         if (!success)
710                 LYXERR0("Cannot create path '" << *this << "'!");
711         return success;
712 }
713
714
715 docstring const FileName::absoluteFilePath() const
716 {
717         return qstring_to_ucs4(d->fi.absoluteFilePath());
718 }
719
720
721 docstring FileName::displayName(int threshold) const
722 {
723         return makeDisplayPath(absFileName(), threshold);
724 }
725
726
727 docstring FileName::fileContents(string const & encoding) const
728 {
729         if (!isReadableFile()) {
730                 LYXERR0("File '" << *this << "' is not readable!");
731                 return docstring();
732         }
733
734         QFile file(d->fi.absoluteFilePath());
735         if (!file.open(QIODevice::ReadOnly)) {
736                 LYXERR0("File '" << *this
737                         << "' could not be opened in read only mode!");
738                 return docstring();
739         }
740         QByteArray contents = file.readAll();
741         file.close();
742
743         if (contents.isEmpty()) {
744                 LYXERR(Debug::FILES, "File '" << *this
745                         << "' is either empty or some error happened while reading it.");
746                 return docstring();
747         }
748
749         QString s;
750         if (encoding.empty() || encoding == "UTF-8")
751                 s = QString::fromUtf8(contents.data());
752         else if (encoding == "ascii")
753 #if (QT_VERSION < 0x050000)
754                 s = QString::fromAscii(contents.data());
755 #else
756                 s = QString::fromLatin1(contents.data());
757 #endif
758         else if (encoding == "local8bit")
759                 s = QString::fromLocal8Bit(contents.data());
760         else if (encoding == "latin1")
761                 s = QString::fromLatin1(contents.data());
762
763         return qstring_to_ucs4(s);
764 }
765
766
767 void FileName::changeExtension(string const & extension)
768 {
769         // FIXME: use Qt native methods...
770         string const oldname = absFileName();
771         string::size_type const last_slash = oldname.rfind('/');
772         string::size_type last_dot = oldname.rfind('.');
773         if (last_dot < last_slash && last_slash != string::npos)
774                 last_dot = string::npos;
775
776         string ext;
777         // Make sure the extension starts with a dot
778         if (!extension.empty() && extension[0] != '.')
779                 ext= '.' + extension;
780         else
781                 ext = extension;
782
783         set(oldname.substr(0, last_dot) + ext);
784 }
785
786
787 docstring const FileName::relPath(string const & path) const
788 {
789         // FIXME UNICODE
790         return makeRelPath(absoluteFilePath(), from_utf8(path));
791 }
792
793
794 // Note: According to Qt, QFileInfo::operator== is undefined when
795 // both files do not exist (Qt4.5 gives true for all non-existent
796 // files, while Qt4.4 compares the filenames).
797 // see:
798 // http://www.qtsoftware.com/developer/task-tracker/
799 //   index_html?id=248471&method=entry.
800 bool equivalent(FileName const & l, FileName const & r)
801 {
802         // FIXME: In future use Qt.
803         // Qt 4.4: We need to solve this warning from Qt documentation:
804         // * Long and short file names that refer to the same file on Windows are
805         //   treated as if they referred to different files.
806         // This is supposed to be fixed for Qt5.
807         FileName const lhs(os::internal_path(l.absFileName()));
808         FileName const rhs(os::internal_path(r.absFileName()));
809
810         if (lhs.empty())
811                 // QFileInfo::operator==() returns false if the two QFileInfo are empty.
812                 return rhs.empty();
813
814         if (rhs.empty())
815                 // Avoid unnecessary checks below.
816                 return false;
817
818         lhs.d->refresh();
819         rhs.d->refresh();
820
821         if (!lhs.d->fi.isSymLink() && !rhs.d->fi.isSymLink()) {
822                 // Qt already checks if the filesystem is case sensitive or not.
823                 // see note above why the extra check with fileName is needed.
824                 return lhs.d->fi == rhs.d->fi
825                         && lhs.d->fi.fileName() == rhs.d->fi.fileName();
826         }
827
828         // FIXME: When/if QFileInfo support symlink comparison, remove this code.
829         QFileInfo fi1(lhs.d->fi);
830         if (fi1.isSymLink())
831                 fi1 = QFileInfo(fi1.symLinkTarget());
832         QFileInfo fi2(rhs.d->fi);
833         if (fi2.isSymLink())
834                 fi2 = QFileInfo(fi2.symLinkTarget());
835         // see note above why the extra check with fileName is needed.
836         return fi1 == fi2 && fi1.fileName() == fi2.fileName();
837 }
838
839
840 bool operator==(FileName const & lhs, FileName const & rhs)
841 {
842         return os::isFilesystemCaseSensitive()
843                 ? lhs.absFileName() == rhs.absFileName()
844                 : !QString::compare(toqstr(lhs.absFileName()),
845                                 toqstr(rhs.absFileName()), Qt::CaseInsensitive);
846 }
847
848
849 bool operator!=(FileName const & lhs, FileName const & rhs)
850 {
851         return !(operator==(lhs, rhs));
852 }
853
854
855 bool operator<(FileName const & lhs, FileName const & rhs)
856 {
857         return lhs.absFileName() < rhs.absFileName();
858 }
859
860
861 bool operator>(FileName const & lhs, FileName const & rhs)
862 {
863         return lhs.absFileName() > rhs.absFileName();
864 }
865
866
867 ostream & operator<<(ostream & os, FileName const & filename)
868 {
869         return os << filename.absFileName();
870 }
871
872
873 /////////////////////////////////////////////////////////////////////
874 //
875 // DocFileName
876 //
877 /////////////////////////////////////////////////////////////////////
878
879
880 DocFileName::DocFileName()
881         : save_abs_path_(true)
882 {}
883
884
885 DocFileName::DocFileName(string const & abs_filename, bool save_abs)
886         : FileName(abs_filename), save_abs_path_(save_abs)
887 {}
888
889
890 DocFileName::DocFileName(FileName const & abs_filename, bool save_abs)
891         : FileName(abs_filename), save_abs_path_(save_abs)
892 {}
893
894
895 void DocFileName::set(string const & name, string const & buffer_path)
896 {
897         save_abs_path_ = isAbsolute(name);
898         if (save_abs_path_)
899                 FileName::set(name);
900         else
901                 FileName::set(makeAbsPath(name, buffer_path).absFileName());
902 }
903
904
905 void DocFileName::erase()
906 {
907         FileName::erase();
908 }
909
910
911 string DocFileName::relFileName(string const & path) const
912 {
913         // FIXME UNICODE
914         return to_utf8(relPath(path));
915 }
916
917
918 string DocFileName::outputFileName(string const & path) const
919 {
920         return save_abs_path_ ? absFileName() : relFileName(path);
921 }
922
923
924 string DocFileName::mangledFileName(string const & dir) const
925 {
926         // Concurrent access to these variables is possible.
927
928         // We need to make sure that every DocFileName instance for a given
929         // filename returns the same mangled name.
930         typedef map<string, string> MangledMap;
931         static MangledMap mangledNames;
932         static Mutex mangledMutex;
933         // this locks both access to mangledNames and counter below
934         Mutex::Locker lock(&mangledMutex);
935         MangledMap::const_iterator const it = mangledNames.find(absFileName());
936         if (it != mangledNames.end())
937                 return (*it).second;
938
939         string const name = absFileName();
940         // Now the real work
941         string mname = os::internal_path(name);
942         // Remove the extension.
943         mname = support::changeExtension(name, string());
944         // The mangled name must be a valid LaTeX name.
945         // The list of characters to keep is probably over-restrictive,
946         // but it is not really a problem.
947         // Apart from non-ASCII characters, at least the following characters
948         // are forbidden: '/', '.', ' ', and ':'.
949         // On windows it is not possible to create files with '<', '>' or '?'
950         // in the name.
951         static string const keep = "abcdefghijklmnopqrstuvwxyz"
952                                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
953                                    "+-0123456789;=";
954         string::size_type pos = 0;
955         while ((pos = mname.find_first_not_of(keep, pos)) != string::npos)
956                 mname[pos++] = '_';
957         // Add the extension back on
958         mname = support::changeExtension(mname, getExtension(name));
959
960         // Prepend a counter to the filename. This is necessary to make
961         // the mangled name unique.
962         static int counter = 0;
963         ostringstream s;
964         s << counter++ << mname;
965         mname = s.str();
966
967         // MiKTeX's YAP (version 2.4.1803) crashes if the file name
968         // is longer than about 160 characters. MiKTeX's pdflatex
969         // is even pickier. A maximum length of 100 has been proven to work.
970         // If dir.size() > max length, all bets are off for YAP. We truncate
971         // the filename nevertheless, keeping a minimum of 10 chars.
972
973         string::size_type max_length = max(100 - ((int)dir.size() + 1), 10);
974
975         // If the mangled file name is too long, hack it to fit.
976         // We know we're guaranteed to have a unique file name because
977         // of the counter.
978         if (mname.size() > max_length) {
979                 int const half = (int(max_length) / 2) - 2;
980                 if (half > 0) {
981                         mname = mname.substr(0, half) + "___" +
982                                 mname.substr(mname.size() - half);
983                 }
984         }
985
986         mangledNames[absFileName()] = mname;
987         return mname;
988 }
989
990
991 string DocFileName::unzippedFileName() const
992 {
993         return support::unzippedFileName(absFileName());
994 }
995
996
997 bool operator==(DocFileName const & lhs, DocFileName const & rhs)
998 {
999         return static_cast<FileName const &>(lhs)
1000                 == static_cast<FileName const &>(rhs)
1001                 && lhs.saveAbsPath() == rhs.saveAbsPath();
1002 }
1003
1004
1005 bool operator!=(DocFileName const & lhs, DocFileName const & rhs)
1006 {
1007         return !(lhs == rhs);
1008 }
1009
1010 } // namespace support
1011 } // namespace lyx