]> git.lyx.org Git - lyx.git/blob - src/support/FileName.cpp
Introducing FileNameList, cleanup some headers and put back dirList() into FileName.
[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/lstrings.h"
19 #include "support/lyxlib.h"
20 #include "support/os.h"
21 #include "support/qstring_helpers.h"
22
23 #include <QDateTime>
24 #include <QDir>
25 #include <QFile>
26 #include <QFileInfo>
27 #include <QList>
28
29 #include <boost/assert.hpp>
30
31 #include <map>
32 #include <sstream>
33 #include <fstream>
34 #include <algorithm>
35
36 #ifdef HAVE_SYS_TYPES_H
37 # include <sys/types.h>
38 #endif
39 #ifdef HAVE_SYS_STAT_H
40 # include <sys/stat.h>
41 #endif
42 #include <cerrno>
43 #include <fcntl.h>
44
45
46 using std::map;
47 using std::string;
48 using std::ifstream;
49 using std::ostringstream;
50 using std::endl;
51
52 namespace lyx {
53 namespace support {
54
55
56 /////////////////////////////////////////////////////////////////////
57 //
58 // FileName::Private
59 //
60 /////////////////////////////////////////////////////////////////////
61
62 struct FileName::Private
63 {
64         Private() {}
65
66         Private(string const & abs_filename) : fi(toqstr(abs_filename))
67         {}
68         ///
69         QFileInfo fi;
70 };
71
72 /////////////////////////////////////////////////////////////////////
73 //
74 // FileName
75 //
76 /////////////////////////////////////////////////////////////////////
77
78
79 FileName::FileName() : d(new Private)
80 {
81 }
82
83
84 FileName::FileName(string const & abs_filename)
85         : d(abs_filename.empty() ? new Private : new Private(abs_filename))
86 {
87 }
88
89
90 FileName::~FileName()
91 {
92         delete d;
93 }
94
95
96 FileName::FileName(FileName const & rhs) : d(new Private)
97 {
98         d->fi = rhs.d->fi;
99 }
100
101
102 FileName & FileName::operator=(FileName const & rhs)
103 {
104         d->fi = rhs.d->fi;
105         return *this;
106 }
107
108
109 bool FileName::empty() const
110 {
111         return d->fi.absoluteFilePath().isEmpty();
112 }
113
114
115 string FileName::absFilename() const
116 {
117         return fromqstr(d->fi.absoluteFilePath());
118 }
119
120
121 void FileName::set(string const & name)
122 {
123         d->fi.setFile(toqstr(name));
124         BOOST_ASSERT(d->fi.isAbsolute());
125 }
126
127
128 void FileName::erase()
129 {
130         d->fi = QFileInfo();
131 }
132
133
134 bool FileName::copyTo(FileName const & name, bool overwrite) const
135 {
136         if (overwrite)
137                 QFile::remove(name.d->fi.absoluteFilePath());
138         bool success = QFile::copy(d->fi.absoluteFilePath(), name.d->fi.absoluteFilePath());
139         if (!success)
140                 lyxerr << "FileName::copyTo(): Could not copy file "
141                         << *this << " to " << name << endl;
142         return success;
143 }
144
145
146 string FileName::toFilesystemEncoding() const
147 {
148         QByteArray const encoded = QFile::encodeName(d->fi.absoluteFilePath());
149         return string(encoded.begin(), encoded.end());
150 }
151
152
153 FileName FileName::fromFilesystemEncoding(string const & name)
154 {
155         QByteArray const encoded(name.c_str(), name.length());
156         return FileName(fromqstr(QFile::decodeName(encoded)));
157 }
158
159
160 bool FileName::exists() const
161 {
162         return d->fi.exists();
163 }
164
165
166 bool FileName::isSymLink() const
167 {
168         return d->fi.isSymLink();
169 }
170
171
172 bool FileName::isFileEmpty() const
173 {
174         return d->fi.size() == 0;
175 }
176
177
178 bool FileName::isDirectory() const
179 {
180         return d->fi.isDir();
181 }
182
183
184 bool FileName::isReadOnly() const
185 {
186         return d->fi.isReadable() && !d->fi.isWritable();
187 }
188
189
190 bool FileName::isReadableDirectory() const
191 {
192         return d->fi.isDir() && d->fi.isReadable();
193 }
194
195
196 std::string FileName::onlyFileName() const
197 {
198         return support::onlyFilename(absFilename());
199 }
200
201
202 FileName FileName::onlyPath() const
203 {
204         return FileName(support::onlyPath(absFilename()));
205 }
206
207
208 bool FileName::isReadableFile() const
209 {
210         return d->fi.isFile() && d->fi.isReadable();
211 }
212
213
214 bool FileName::isWritable() const
215 {
216         return d->fi.isWritable();
217 }
218
219
220 bool FileName::isDirWritable() const
221 {
222         LYXERR(Debug::FILES, "isDirWriteable: " << *this);
223
224         FileName const tmpfl(tempName(*this, "lyxwritetest"));
225
226         if (tmpfl.empty())
227                 return false;
228
229         tmpfl.removeFile();
230         return true;
231 }
232
233
234 FileNameList FileName::dirList(std::string const & ext) const
235 {
236         FileNameList dirlist;
237         if (!isDirectory()) {
238                 LYXERR0("Directory '" << *this << "' does not exist!");
239                 return dirlist;
240         }
241
242         QDir dir = d->fi.absoluteDir();
243
244         if (!ext.empty()) {
245                 QString filter;
246                 switch (ext[0]) {
247                 case '.': filter = "*" + toqstr(ext); break;
248                 case '*': filter = toqstr(ext); break;
249                 default: filter = "*." + toqstr(ext);
250                 }
251                 dir.setNameFilters(QStringList(filter));
252                 LYXERR(Debug::FILES, "filtering on extension "
253                         << fromqstr(filter) << " is requested.");
254         }
255
256         QFileInfoList list = dir.entryInfoList();
257         for (int i = 0; i != list.size(); ++i) {
258                 FileName fi(fromqstr(list.at(i).absoluteFilePath()));
259                 dirlist.push_back(fi);
260                 LYXERR(Debug::FILES, "found file " << fi);
261         }
262
263         return dirlist;
264 }
265
266
267 FileName FileName::tempName(FileName const & dir, std::string const & mask)
268 {
269         return support::tempName(dir, mask);
270 }
271
272
273 std::time_t FileName::lastModified() const
274 {
275         return d->fi.lastModified().toTime_t();
276 }
277
278
279 bool FileName::chdir() const
280 {
281         return QDir::setCurrent(d->fi.absoluteFilePath());
282 }
283
284
285 extern unsigned long sum(char const * file);
286
287 unsigned long FileName::checksum() const
288 {
289         if (!exists()) {
290                 LYXERR0("File \"" << absFilename() << "\" does not exist!");
291                 return 0;
292         }
293         // a directory may be passed here so we need to test it. (bug 3622)
294         if (isDirectory()) {
295                 LYXERR0('\\' << absFilename() << "\" is a directory!");
296                 return 0;
297         }
298         LYXERR0("Checksumming \"" << absFilename() << "\".");
299         return sum(absFilename().c_str());
300 }
301
302
303 bool FileName::removeFile() const
304 {
305         bool const success = QFile::remove(d->fi.absoluteFilePath());
306         if (!success && exists())
307                 lyxerr << "FileName::removeFile(): Could not delete file "
308                         << *this << "." << endl;
309         return success;
310 }
311
312
313 static bool rmdir(QFileInfo const & fi)
314 {
315         QDir dir(fi.absoluteFilePath());
316         QFileInfoList list = dir.entryInfoList();
317         bool global_success = true;
318         for (int i = 0; i != list.size(); ++i) {
319                 if (list.at(i).fileName() == ".")
320                         continue;
321                 if (list.at(i).fileName() == "..")
322                         continue;
323                 bool success;
324                 if (list.at(i).isDir()) {
325                         LYXERR(Debug::FILES, "Erasing dir " 
326                                 << fromqstr(list.at(i).absoluteFilePath()));
327                         success = rmdir(list.at(i));
328                 }
329                 else {
330                         LYXERR(Debug::FILES, "Erasing file " 
331                                 << fromqstr(list.at(i).absoluteFilePath()));
332                         success = dir.remove(list.at(i).fileName());
333                 }
334                 if (!success) {
335                         global_success = false;
336                         lyxerr << "Could not delete "
337                                 << fromqstr(list.at(i).absoluteFilePath()) << "." << endl;
338                 }
339         } 
340         QDir parent = fi.absolutePath();
341         global_success |= parent.rmdir(fi.fileName());
342         return global_success;
343 }
344
345
346 bool FileName::destroyDirectory() const
347 {
348         bool const success = rmdir(d->fi);
349         if (!success)
350                 lyxerr << "Could not delete " << *this << "." << endl;
351
352         return success;
353 }
354
355
356 bool FileName::createDirectory(int permission) const
357 {
358         BOOST_ASSERT(!empty());
359         return mkdir(*this, permission) == 0;
360 }
361
362
363 docstring const FileName::absoluteFilePath() const
364 {
365         return qstring_to_ucs4(d->fi.absoluteFilePath());
366 }
367
368
369 docstring FileName::displayName(int threshold) const
370 {
371         return makeDisplayPath(absFilename(), threshold);
372 }
373
374
375 docstring FileName::fileContents(string const & encoding) const
376 {
377         if (!isReadableFile()) {
378                 LYXERR0("File '" << *this
379                         << "' is not redable!");
380                 return docstring();
381         }
382
383         QFile file(d->fi.absoluteFilePath());
384         if (!file.open(QIODevice::ReadOnly)) {
385                 LYXERR0("File '" << *this
386                         << "' could not be opened in read only mode!");
387                 return docstring();
388         }
389         QByteArray contents = file.readAll();
390         file.close();
391
392         if (contents.isEmpty()) {
393                 LYXERR(Debug::FILES, "File '" << *this
394                         << "' is either empty or some error happened while reading it.");
395                 return docstring();
396         }
397
398         QString s;
399         if (encoding.empty() || encoding == "UTF-8")
400                 s = QString::fromUtf8(contents.data());
401         else if (encoding == "ascii")
402                 s = QString::fromAscii(contents.data());
403         else if (encoding == "local8bit")
404                 s = QString::fromLocal8Bit(contents.data());
405         else if (encoding == "latin1")
406                 s = QString::fromLatin1(contents.data());
407
408         return qstring_to_ucs4(s);
409 }
410
411
412 void FileName::changeExtension(std::string const & extension)
413 {
414         // FIXME: use Qt native methods...
415         string const oldname = absFilename();
416         string::size_type const last_slash = oldname.rfind('/');
417         string::size_type last_dot = oldname.rfind('.');
418         if (last_dot < last_slash && last_slash != string::npos)
419                 last_dot = string::npos;
420
421         string ext;
422         // Make sure the extension starts with a dot
423         if (!extension.empty() && extension[0] != '.')
424                 ext= '.' + extension;
425         else
426                 ext = extension;
427
428         set(oldname.substr(0, last_dot) + ext);
429 }
430
431
432 string FileName::guessFormatFromContents() const
433 {
434         // the different filetypes and what they contain in one of the first lines
435         // (dots are any characters).           (Herbert 20020131)
436         // AGR  Grace...
437         // BMP  BM...
438         // EPS  %!PS-Adobe-3.0 EPSF...
439         // FIG  #FIG...
440         // FITS ...BITPIX...
441         // GIF  GIF...
442         // JPG  JFIF
443         // PDF  %PDF-...
444         // PNG  .PNG...
445         // PBM  P1... or P4     (B/W)
446         // PGM  P2... or P5     (Grayscale)
447         // PPM  P3... or P6     (color)
448         // PS   %!PS-Adobe-2.0 or 1.0,  no "EPSF"!
449         // SGI  \001\332...     (decimal 474)
450         // TGIF %TGIF...
451         // TIFF II... or MM...
452         // XBM  ..._bits[]...
453         // XPM  /* XPM */    sometimes missing (f.ex. tgif-export)
454         //      ...static char *...
455         // XWD  \000\000\000\151        (0x00006900) decimal 105
456         //
457         // GZIP \037\213        http://www.ietf.org/rfc/rfc1952.txt
458         // ZIP  PK...                   http://www.halyava.ru/document/ind_arch.htm
459         // Z    \037\235                UNIX compress
460         // paranoia check
461
462         if (empty() || !isReadableFile())
463                 return string();
464
465         ifstream ifs(toFilesystemEncoding().c_str());
466         if (!ifs)
467                 // Couldn't open file...
468                 return string();
469
470         // gnuzip
471         static string const gzipStamp = "\037\213";
472
473         // PKZIP
474         static string const zipStamp = "PK";
475
476         // compress
477         static string const compressStamp = "\037\235";
478
479         // Maximum strings to read
480         int const max_count = 50;
481         int count = 0;
482
483         string str;
484         string format;
485         bool firstLine = true;
486         while ((count++ < max_count) && format.empty()) {
487                 if (ifs.eof()) {
488                         LYXERR(Debug::GRAPHICS, "filetools(getFormatFromContents)\n"
489                                 << "\tFile type not recognised before EOF!");
490                         break;
491                 }
492
493                 getline(ifs, str);
494                 string const stamp = str.substr(0, 2);
495                 if (firstLine && str.size() >= 2) {
496                         // at first we check for a zipped file, because this
497                         // information is saved in the first bytes of the file!
498                         // also some graphic formats which save the information
499                         // in the first line, too.
500                         if (prefixIs(str, gzipStamp)) {
501                                 format =  "gzip";
502
503                         } else if (stamp == zipStamp) {
504                                 format =  "zip";
505
506                         } else if (stamp == compressStamp) {
507                                 format =  "compress";
508
509                         // the graphics part
510                         } else if (stamp == "BM") {
511                                 format =  "bmp";
512
513                         } else if (stamp == "\001\332") {
514                                 format =  "sgi";
515
516                         // PBM family
517                         // Don't need to use str.at(0), str.at(1) because
518                         // we already know that str.size() >= 2
519                         } else if (str[0] == 'P') {
520                                 switch (str[1]) {
521                                 case '1':
522                                 case '4':
523                                         format =  "pbm";
524                                     break;
525                                 case '2':
526                                 case '5':
527                                         format =  "pgm";
528                                     break;
529                                 case '3':
530                                 case '6':
531                                         format =  "ppm";
532                                 }
533                                 break;
534
535                         } else if ((stamp == "II") || (stamp == "MM")) {
536                                 format =  "tiff";
537
538                         } else if (prefixIs(str,"%TGIF")) {
539                                 format =  "tgif";
540
541                         } else if (prefixIs(str,"#FIG")) {
542                                 format =  "fig";
543
544                         } else if (prefixIs(str,"GIF")) {
545                                 format =  "gif";
546
547                         } else if (str.size() > 3) {
548                                 int const c = ((str[0] << 24) & (str[1] << 16) &
549                                                (str[2] << 8)  & str[3]);
550                                 if (c == 105) {
551                                         format =  "xwd";
552                                 }
553                         }
554
555                         firstLine = false;
556                 }
557
558                 if (!format.empty())
559                     break;
560                 else if (contains(str,"EPSF"))
561                         // dummy, if we have wrong file description like
562                         // %!PS-Adobe-2.0EPSF"
563                         format = "eps";
564
565                 else if (contains(str, "Grace"))
566                         format = "agr";
567
568                 else if (contains(str, "JFIF"))
569                         format = "jpg";
570
571                 else if (contains(str, "%PDF"))
572                         format = "pdf";
573
574                 else if (contains(str, "PNG"))
575                         format = "png";
576
577                 else if (contains(str, "%!PS-Adobe")) {
578                         // eps or ps
579                         ifs >> str;
580                         if (contains(str,"EPSF"))
581                                 format = "eps";
582                         else
583                             format = "ps";
584                 }
585
586                 else if (contains(str, "_bits[]"))
587                         format = "xbm";
588
589                 else if (contains(str, "XPM") || contains(str, "static char *"))
590                         format = "xpm";
591
592                 else if (contains(str, "BITPIX"))
593                         format = "fits";
594         }
595
596         if (!format.empty()) {
597                 LYXERR(Debug::GRAPHICS, "Recognised Fileformat: " << format);
598                 return format;
599         }
600
601         LYXERR(Debug::GRAPHICS, "filetools(getFormatFromContents)\n"
602                 << "\tCouldn't find a known format!");
603         return string();
604 }
605
606
607 bool FileName::isZippedFile() const
608 {
609         string const type = guessFormatFromContents();
610         return contains("gzip zip compress", type) && !type.empty();
611 }
612
613
614 docstring const FileName::relPath(string const & path) const
615 {
616         // FIXME UNICODE
617         return makeRelPath(absoluteFilePath(), from_utf8(path));
618 }
619
620
621 bool operator==(FileName const & lhs, FileName const & rhs)
622 {
623         return lhs.absFilename() == rhs.absFilename();
624 }
625
626
627 bool operator!=(FileName const & lhs, FileName const & rhs)
628 {
629         return lhs.absFilename() != rhs.absFilename();
630 }
631
632
633 bool operator<(FileName const & lhs, FileName const & rhs)
634 {
635         return lhs.absFilename() < rhs.absFilename();
636 }
637
638
639 bool operator>(FileName const & lhs, FileName const & rhs)
640 {
641         return lhs.absFilename() > rhs.absFilename();
642 }
643
644
645 std::ostream & operator<<(std::ostream & os, FileName const & filename)
646 {
647         return os << filename.absFilename();
648 }
649
650
651 /////////////////////////////////////////////////////////////////////
652 //
653 // DocFileName
654 //
655 /////////////////////////////////////////////////////////////////////
656
657
658 DocFileName::DocFileName()
659         : save_abs_path_(true)
660 {}
661
662
663 DocFileName::DocFileName(string const & abs_filename, bool save_abs)
664         : FileName(abs_filename), save_abs_path_(save_abs), zipped_valid_(false)
665 {}
666
667
668 DocFileName::DocFileName(FileName const & abs_filename, bool save_abs)
669         : FileName(abs_filename), save_abs_path_(save_abs), zipped_valid_(false)
670 {}
671
672
673 void DocFileName::set(string const & name, string const & buffer_path)
674 {
675         save_abs_path_ = absolutePath(name);
676         FileName::set(save_abs_path_ ? name : makeAbsPath(name, buffer_path).absFilename());
677         zipped_valid_ = false;
678 }
679
680
681 void DocFileName::erase()
682 {
683         FileName::erase();
684         zipped_valid_ = false;
685 }
686
687
688 string DocFileName::relFilename(string const & path) const
689 {
690         // FIXME UNICODE
691         return to_utf8(relPath(path));
692 }
693
694
695 string DocFileName::outputFilename(string const & path) const
696 {
697         return save_abs_path_ ? absFilename() : relFilename(path);
698 }
699
700
701 string DocFileName::mangledFilename(std::string const & dir) const
702 {
703         // We need to make sure that every DocFileName instance for a given
704         // filename returns the same mangled name.
705         typedef map<string, string> MangledMap;
706         static MangledMap mangledNames;
707         MangledMap::const_iterator const it = mangledNames.find(absFilename());
708         if (it != mangledNames.end())
709                 return (*it).second;
710
711         string const name = absFilename();
712         // Now the real work
713         string mname = os::internal_path(name);
714         // Remove the extension.
715         mname = support::changeExtension(name, string());
716         // The mangled name must be a valid LaTeX name.
717         // The list of characters to keep is probably over-restrictive,
718         // but it is not really a problem.
719         // Apart from non-ASCII characters, at least the following characters
720         // are forbidden: '/', '.', ' ', and ':'.
721         // On windows it is not possible to create files with '<', '>' or '?'
722         // in the name.
723         static string const keep = "abcdefghijklmnopqrstuvwxyz"
724                                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
725                                    "+,-0123456789;=";
726         string::size_type pos = 0;
727         while ((pos = mname.find_first_not_of(keep, pos)) != string::npos)
728                 mname[pos++] = '_';
729         // Add the extension back on
730         mname = support::changeExtension(mname, getExtension(name));
731
732         // Prepend a counter to the filename. This is necessary to make
733         // the mangled name unique.
734         static int counter = 0;
735         std::ostringstream s;
736         s << counter++ << mname;
737         mname = s.str();
738
739         // MiKTeX's YAP (version 2.4.1803) crashes if the file name
740         // is longer than about 160 characters. MiKTeX's pdflatex
741         // is even pickier. A maximum length of 100 has been proven to work.
742         // If dir.size() > max length, all bets are off for YAP. We truncate
743         // the filename nevertheless, keeping a minimum of 10 chars.
744
745         string::size_type max_length = std::max(100 - ((int)dir.size() + 1), 10);
746
747         // If the mangled file name is too long, hack it to fit.
748         // We know we're guaranteed to have a unique file name because
749         // of the counter.
750         if (mname.size() > max_length) {
751                 int const half = (int(max_length) / 2) - 2;
752                 if (half > 0) {
753                         mname = mname.substr(0, half) + "___" +
754                                 mname.substr(mname.size() - half);
755                 }
756         }
757
758         mangledNames[absFilename()] = mname;
759         return mname;
760 }
761
762
763 bool DocFileName::isZipped() const
764 {
765         if (!zipped_valid_) {
766                 zipped_ = isZippedFile();
767                 zipped_valid_ = true;
768         }
769         return zipped_;
770 }
771
772
773 string DocFileName::unzippedFilename() const
774 {
775         return unzippedFileName(absFilename());
776 }
777
778
779 bool operator==(DocFileName const & lhs, DocFileName const & rhs)
780 {
781         return lhs.absFilename() == rhs.absFilename()
782                 && lhs.saveAbsPath() == rhs.saveAbsPath();
783 }
784
785
786 bool operator!=(DocFileName const & lhs, DocFileName const & rhs)
787 {
788         return !(lhs == rhs);
789 }
790
791 } // namespace support
792 } // namespace lyx