seeking out problem file.
[feisty_meow.git] / nucleus / library / filesystem / filename.cpp
1 /*****************************************************************************\
2 *                                                                             *
3 *  Name   : filename                                                          *
4 *  Author : Chris Koeritz                                                     *
5 *                                                                             *
6 *******************************************************************************
7 * Copyright (c) 1993-$now By Author.  This program is free software; you can  *
8 * redistribute it and/or modify it under the terms of the GNU General Public  *
9 * License as published by the Free Software Foundation; either version 2 of   *
10 * the License or (at your option) any later version.  This is online at:      *
11 *     http://www.fsf.org/copyleft/gpl.html                                    *
12 * Please send any updates to: fred@gruntose.com                               *
13 \*****************************************************************************/
14
15 // implementation note: the filename is kept canonicalized.  any constructor
16 // or assignment operator should ensure this (except the blank constructor).
17
18 #include "filename.h"
19
20 #include <basis/byte_array.h>
21 #include <basis/functions.h>
22 #include <textual/parser_bits.h>
23
24 #include <stdio.h>
25 #include <sys/stat.h>
26 #include <sys/types.h>
27 #ifdef __UNIX__
28   #include <unistd.h>
29 #endif
30 #ifdef __WIN32__
31   #include <io.h>
32 #endif
33
34 #undef LOG
35 #define LOG(to_print) printf("%s::%s: %s\n", static_class_name(), func, astring(to_print).s())
36
37 using namespace basis;
38 using namespace structures;
39
40 class status_info : public stat
41 {
42 };
43
44 namespace filesystem {
45
46 #if defined(__WIN32__) || defined(__VMS__)
47   const char DEFAULT_SEPARATOR = '\\';
48 #elif defined(__UNIX__)
49   const char DEFAULT_SEPARATOR = '/';
50 #else
51   #error "We have no idea what the default path separator is."
52 #endif
53
54 const char *NO_PARENT_DEFAULT = ".";
55   // used when no directory name can be popped off.
56
57 filename::filename()
58 : astring(),
59   _had_directory(false)
60 {}
61
62 filename::filename(const astring &name)
63 : astring(name),
64   _had_directory(true)
65 { canonicalize(); }
66
67 filename::filename(const astring &directory, const astring &name_of_file)
68 : astring(directory),
69   _had_directory(true)
70 {
71   // if the directory is empty, use the current directory.
72   if (!directory) {
73     *this = astring(NO_PARENT_DEFAULT);
74     _had_directory = false;
75   }
76   // check for a slash on the end of the directory.  add one if there is none
77   // currently.
78   bool add_slash = false;
79   if ( (directory[directory.end()] != '\\')
80        && (directory[directory.end()] != '/') ) add_slash = true;
81   if (add_slash) *this += DEFAULT_SEPARATOR;
82   *this += name_of_file;
83   canonicalize();
84 }
85
86 filename::filename(const filename &to_copy)
87 : astring(to_copy),
88   _had_directory(to_copy._had_directory)
89 { canonicalize(); }
90
91 filename::~filename() {}
92
93 astring filename::default_separator() { return astring(DEFAULT_SEPARATOR, 1); }
94
95 astring &filename::raw() { return *this; }
96
97 const astring &filename::raw() const { return *this; }
98
99 bool filename::good() const { return exists(); }
100
101 bool filename::unlink() const { return ::unlink(observe()) == 0; }
102
103 void filename::reset(const astring &name) {
104   *this = name;
105   _had_directory = true;  // until we know better.
106   canonicalize();
107 }
108
109 astring filename::null_device()
110 {
111 #ifdef __WIN32__
112   return "null:";
113 #else
114   return "/dev/null";
115 #endif
116 }
117
118 bool filename::separator(char is_it)
119 { return (is_it == pc_separator) || (is_it == unix_separator); }
120
121 filename &filename::operator = (const filename &to_copy)
122 {
123   if (this == &to_copy) return *this;
124   (astring &)(*this) = to_copy;
125   _had_directory = to_copy._had_directory;
126   return *this;
127 }
128
129 filename &filename::operator = (const astring &to_copy)
130 {
131   _had_directory = true;
132   if (this == &to_copy) return *this;
133   (astring &)(*this) = to_copy;
134   canonicalize();
135   return *this;
136 }
137
138 astring filename::pop()
139 {
140   astring to_return = basename();
141   filename parent_dir = parent();
142   if (parent_dir.raw().equal_to(NO_PARENT_DEFAULT)) {
143     // we haven't gone anywhere.
144     return "";  // signal that nothing was removed.
145   }
146   *this = parent_dir;
147   return to_return;
148 }
149
150 filename filename::parent() const { return dirname(); }
151
152 void filename::push(const astring &to_push)
153 {
154   *this = filename(*this, to_push);
155 }
156
157 void filename::canonicalize()
158 {
159   FUNCDEF("canonicalize");
160   // turn all the non-default separators into the default.
161   bool found_sep = false;
162   for (int j = 0; j < length(); j++) {
163     if (separator(get(j))) {
164       found_sep = true;
165       put(j, DEFAULT_SEPARATOR);
166     }
167   }
168
169   // if there wasn't a single directory separator, then they must not have
170   // specified any directory name for this filename (although it could itself
171   // be a directory).
172   if (!found_sep) _had_directory = false;
173
174   // remove all occurrences of double separators except for the first
175   // double set, which could be a UNC filename.  that's why the index below
176   // starts at one rather than zero.
177   bool saw_sep = false;
178   for (int i = 1; i < length(); i++) {
179     if (separator(get(i))) {
180       if (saw_sep) {
181         zap(i, i);
182           // two in a row is no good, except for the first two.
183         i--;  // skip back one and try again.
184         continue;
185       }
186       saw_sep = true;
187     } else saw_sep = false;
188   }
189
190 #ifdef __WIN32__
191   // on windows, we want to translate away from any cygwin or msys format into a more palatable
192   // version that the rest of windows understands.
193   // first, cygwin...
194   const astring CYGDRIVE_PATH = astring(astring(DEFAULT_SEPARATOR, 1) + "cygdrive"
195       + astring(DEFAULT_SEPARATOR, 1));
196   // must be at least as long as the string we're looking for, plus a drive letter afterwards.
197   if ( (length() > CYGDRIVE_PATH.length() + 1) && begins(CYGDRIVE_PATH) ) {
198     zap(0, CYGDRIVE_PATH.length() - 1);  // whack the cygdrive portion plus two slashes.
199     insert(1, ":");  // add a colon after the imputed drive letter.
200 LOG(astring("turned cygdrive string into: ") + *this);
201   }
202   // now we convert msys...
203   if ( (length() >= 2) && (get(0) == DEFAULT_SEPARATOR)
204         && textual::parser_bits::is_alpha(get(1)) ) {
205     // we seem reasonably sure now that this is a windows path hiding in msys format, but
206     // the next character needs to be a slash (if there is a next character) for it to be
207     // the windows drive form.  otherwise it could be /tmp, which would obviously not be
208     // intended as a windows path.
209     if ( (length() == 2) || (get(2) == DEFAULT_SEPARATOR) ) {
210       // cool, this should be interpretable as an msys path, except for those wacky types
211       // of folks that might use a top-level single character directory name.  we cannot
212       // help them, because we have made a design decision to support msys-style paths.
213       // note that this would only affect someone if they were referring to their directory on
214       // the current windows partition (c:, d:, etc.) without providing the drive letter,
215       // if they had that single character directory name (e.g., c:\x, d:\q, etc.) and even
216       // then only on the near defunct windows platform.
217       zap(0, 0);  // take off initial slash.
218       insert(1, ":");  // add the obligatory colon.
219 LOG(astring("turned msys string into: ") + *this);
220     }
221   } 
222 #endif
223
224   // we don't crop the last separator if the name's too small.  for msdos
225   // names, that would be chopping a slash off the c:\ style name.
226   if (length() > 3) {
227     // zap any separators that are hiding on the end.
228     const int last = end();
229     if (separator(get(last))) zap(last, last);
230   } else if ( (length() == 2) && (get(1) == ':') ) {
231     // special case for dos drive names.  we turn it back into a valid
232     // directory rather than leaving it as just "X:".  that form of the name
233     // means something else under dos/windows.
234     *this += astring(DEFAULT_SEPARATOR, 1);
235   }
236 }
237
238 char filename::drive(bool interact_with_fs) const
239 {
240   // first guess: if second letter's a colon, first letter's the drive.
241   if (length() < 2)
242     return '\0';
243   if (get(1) == ':')
244     return get(0);
245   if (!interact_with_fs)
246     return '\0';
247
248   // otherwise, retrieve the file system's record for the file.
249   status_info fill;
250   if (!get_info(&fill))
251     return '\0';
252   return char('A' + fill.st_dev);
253 }
254
255 astring filename::extension() const
256 {
257   astring base(basename().raw());
258   int posn = base.find('.', base.end(), true);
259   if (negative(posn))
260     return "";
261   return base.substring(posn + 1, base.length() - 1);
262 }
263
264 astring filename::rootname() const
265 {
266   astring base(basename().raw());
267   int posn = base.find('.', base.end(), true);
268   if (negative(posn))
269     return base;
270   return base.substring(0, posn - 1);
271 }
272
273 bool filename::get_info(status_info *to_fill) const
274 {
275   int ret = stat(observe(), to_fill);
276   if (ret)
277     return false;
278   return true;
279 }
280
281 bool filename::is_directory() const
282 {
283   status_info fill;
284   if (!get_info(&fill))
285     return false;
286   return !!(fill.st_mode & S_IFDIR);
287 }
288
289 bool filename::is_writable() const
290 {
291   status_info fill;
292   if (!get_info(&fill))
293     return false;
294   return !!(fill.st_mode & S_IWRITE);
295 }
296
297 bool filename::is_readable() const
298 {
299   status_info fill;
300   if (!get_info(&fill))
301     return false;
302   return !!(fill.st_mode & S_IREAD);
303 }
304
305 bool filename::is_executable() const
306 {
307   status_info fill;
308   if (!get_info(&fill))
309     return false;
310   return !!(fill.st_mode & S_IEXEC);
311 }
312
313 bool filename::is_normal() const
314 {
315   status_info fill;
316   if (!get_info(&fill))
317     return false;
318 #if defined(__WIN32__) || defined(__VMS__)
319 //hmmm: is there a corresponding set of functions for windows, where applicable?
320   bool weird = false;
321 #else
322   bool weird = S_ISCHR(fill.st_mode)
323       || S_ISBLK(fill.st_mode)
324       || S_ISFIFO(fill.st_mode)
325       || S_ISSOCK(fill.st_mode);
326 #endif
327   return !weird;
328 }
329
330 int filename::find_last_separator(const astring &look_at) const
331 {
332   int last_sep = -1;
333   int sep = 0;
334   while (sep >= 0) {
335     sep = look_at.find(DEFAULT_SEPARATOR, last_sep + 1);
336     if (sep >= 0) last_sep = sep;
337   }
338   return last_sep;
339 }
340
341 filename filename::basename() const
342 {
343   astring basename = *this;
344   int last_sep = find_last_separator(basename);
345   if (last_sep >= 0) basename.zap(0, last_sep);
346   return basename;
347 }
348
349 filename filename::dirname() const
350 {
351   astring dirname = *this;
352   int last_sep = find_last_separator(dirname);
353   // we don't accept ripping off the first slash.
354   if (last_sep >= 1) {
355     // we can rip the slash and suffix off to get the directory name.  however,
356     // this might be in the form X: on windows.  if they want the slash to
357     // remain, they can use the dirname that appends it.
358     dirname.zap(last_sep, dirname.end());
359   } else {
360     if (get(0) == DEFAULT_SEPARATOR) {
361       // handle when we're up at the top of the filesystem.  on unix, once
362       // you hit the root, you can keep going up but you still remain at
363       // the root.  similarly on windoze, if there's no drive name in there.
364       dirname = astring(DEFAULT_SEPARATOR, 1);
365     } else {
366       // there's no slash at all in the filename any more.  we assume that
367       // the directory is the current one, if no other information is
368       // available.  this default is already used by some code.
369       dirname = NO_PARENT_DEFAULT;
370     }
371   }
372   return dirname;
373 }
374
375 astring filename::dirname(bool add_slash) const
376 {
377   astring tempname = dirname().raw();
378   if (add_slash) tempname += DEFAULT_SEPARATOR;
379   return tempname;
380 }
381
382 bool filename::exists() const
383 {
384   if (is_directory())
385     return true;
386   // if the file name is empty, that cannot exist.
387   if (!length())
388     return false;
389   return is_readable();
390 }
391
392 bool filename::legal_character(char to_check)
393 {
394   switch (to_check) {
395     case ':': case ';':
396     case '\\': case '/':
397     case '*': case '?': case '$': case '&': case '|':
398     case '\'': case '"': case '`':
399     case '(': case ')':
400     case '[': case ']':
401     case '<': case '>':
402     case '{': case '}':
403       return false;
404     default: return true;
405   }
406 }
407
408 void filename::detooth_filename(astring &to_clean, char replacement)
409 {
410   for (int i = 0; i < to_clean.length(); i++) {
411     if (!legal_character(to_clean[i]))
412       to_clean[i] = replacement;
413   }
414 }
415
416 int filename::packed_size() const
417 {
418   return PACKED_SIZE_INT32 + astring::packed_size();
419 }
420
421 void filename::pack(byte_array &packed_form) const
422 {
423   attach(packed_form, int(_had_directory));
424   astring::pack(packed_form);
425 }
426
427 bool filename::unpack(byte_array &packed_form)
428 {
429   int temp;
430   if (!detach(packed_form, temp))
431     return false;
432   _had_directory = temp;
433   if (!astring::unpack(packed_form))
434     return false;
435   return true;
436 }
437
438 void filename::separate(bool &rooted, string_array &pieces) const
439 {
440   pieces.reset();
441   const astring &raw_form = raw();
442   astring accumulator;  // holds the names we find.
443   rooted = raw_form.length() && separator(raw_form[0]);
444   for (int i = 0; i < raw_form.length(); i++) {
445     if (separator(raw_form[i])) {
446       // this is a separator character, so eat it and add the accumulated
447       // string to the list.
448       if (i && accumulator.length()) pieces += accumulator;
449       // now reset our accumulated text.
450       accumulator = astring::empty_string();
451     } else {
452       // not a separator, so just accumulate it.
453       accumulator += raw_form[i];
454     }
455   }
456   if (accumulator.length()) pieces += accumulator;
457 }
458
459 void filename::join(bool rooted, const string_array &pieces)
460 {
461   astring constructed_name;  // we'll make a filename here.
462   if (rooted) constructed_name += DEFAULT_SEPARATOR;
463   for (int i = 0; i < pieces.length(); i++) {
464     constructed_name += pieces[i];
465     if (!i || (i != pieces.length() - 1))
466       constructed_name += DEFAULT_SEPARATOR;
467   }
468   *this = constructed_name;
469 }
470
471 bool filename::base_compare_prefix(const filename &to_compare,
472     string_array &first, string_array &second)
473 {
474   bool first_rooted;
475   separate(first_rooted, first);
476   bool second_rooted;
477   to_compare.separate(second_rooted, second);
478   if (first_rooted != second_rooted) {
479     return false;
480   }
481   // that case should never be allowed, since there are some bits missing
482   // in the name to be compared.
483   if (first.length() > second.length())
484     return false;
485
486   // compare each of the pieces.
487   for (int i = 0; i < first.length(); i++) {
488 #if defined(__WIN32__) || defined(__VMS__)
489     // case-insensitive compare.
490     if (!first[i].iequals(second[i]))
491       return false;
492 #else
493     // case-sensitive compare.
494     if (first[i] != second[i])
495       return false;
496 #endif
497   }
498   return true;
499 }
500
501 bool filename::compare_prefix(const filename &to_compare, astring &sequel)
502 {
503   sequel = astring::empty_string();  // clean our output parameter.
504   string_array first;
505   string_array second;
506   if (!base_compare_prefix(to_compare, first, second))
507     return false;
508
509   // create the sequel string.
510   int extra_strings = second.length() - first.length();
511   for (int i = second.length() - extra_strings; i < second.length(); i++) {
512     sequel += second[i];
513     if (i != second.length() - 1) sequel += DEFAULT_SEPARATOR;
514   }
515
516   return true;
517 }
518
519 bool filename::compare_prefix(const filename &to_compare)
520 {
521   string_array first;
522   string_array second;
523   return base_compare_prefix(to_compare, first, second);
524 }
525
526 bool filename::base_compare_suffix(const filename &to_compare,
527     string_array &first, string_array &second)
528 {
529   bool first_rooted;
530   separate(first_rooted, first);
531   bool second_rooted;
532   to_compare.separate(second_rooted, second);
533   // that case should never be allowed, since there are some bits missing
534   // in the name to be compared.
535   if (first.length() > second.length())
536     return false;
537
538   // compare each of the pieces.
539   for (int i = first.length() - 1; i >= 0; i--) {
540 //clean up this computation; the difference in lengths is constant--use that.
541     int distance_from_end = first.length() - 1 - i;
542     int j = second.length() - 1 - distance_from_end;
543 #if defined(__WIN32__) || defined(__VMS__)
544     // case-insensitive compare.
545     if (!first[i].iequals(second[j]))
546       return false;
547 #else
548     // case-sensitive compare.
549     if (first[i] != second[j])
550       return false;
551 #endif
552   }
553   return true;
554 }
555
556 bool filename::compare_suffix(const filename &to_compare, astring &prequel)
557 {
558   prequel = astring::empty_string();  // clean our output parameter.
559   string_array first;
560   string_array second;
561   if (!base_compare_suffix(to_compare, first, second))
562     return false;
563
564   // create the prequel string.
565   int extra_strings = second.length() - first.length();
566   for (int i = 0; i < extra_strings; i++) {
567     prequel += second[i];
568     if (i != second.length() - 1) prequel += DEFAULT_SEPARATOR;
569   }
570   return true;
571 }
572
573 bool filename::compare_suffix(const filename &to_compare)
574 {
575   string_array first;
576   string_array second;
577   return base_compare_suffix(to_compare, first, second);
578 }
579
580 bool filename::chmod(int write_mode, int owner_mode) const
581 {
582   int chmod_value = 0;
583 #ifdef __UNIX__
584   if (write_mode & ALLOW_READ) {
585     if (owner_mode & USER_RIGHTS) chmod_value |= S_IRUSR;
586     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IRGRP;
587     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IROTH;
588   }
589   if (write_mode & ALLOW_WRITE) {
590     if (owner_mode & USER_RIGHTS) chmod_value |= S_IWUSR;
591     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IWGRP;
592     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IWOTH;
593   }
594 ////  chmod_value = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
595 #elif defined(__WIN32__)
596   if (write_mode & ALLOW_READ) {
597     chmod_value |= _S_IREAD;
598   }
599   if (write_mode & ALLOW_WRITE) {
600     chmod_value |= _S_IWRITE;
601   }
602 #else
603   #error unsupported OS type currently.
604 #endif
605   int chmod_result = ::chmod(raw().s(), chmod_value);
606   if (chmod_result) {
607 //    LOG(astring("there was a problem changing permissions on ") + raw());
608     return false;
609   }
610   return true;
611 }
612
613 } //namespace.
614