4abca2ade98b462c6f75404f1e34baf54fd0b6d6
[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 #include <system_helper.h>
24
25 #include <stdio.h>
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #if defined(__UNIX__) || defined(__GNU_WINDOWS__)
29   #include <unistd.h>
30 #else
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 //hmmm: make these into statics!
195   const astring CYGDRIVE_SENTINEL = "cygdrive";
196   const astring CYGDRIVE_PATH = astring(astring(DEFAULT_SEPARATOR, 1)
197       + CYGDRIVE_SENTINEL + astring(DEFAULT_SEPARATOR, 1));
198
199   // must be at least as long as the string we're looking for, plus a drive letter afterwards.
200   if ( (length() >= CYGDRIVE_PATH.length() + 1)
201       && separator(get(0))
202       && separator(get(CYGDRIVE_PATH.length() - 1))
203       && compare(CYGDRIVE_SENTINEL, 1, 
204           0, CYGDRIVE_SENTINEL.length(), true) ) {
205     zap(0, CYGDRIVE_PATH.length() - 1);  // whack the cygdrive portion plus two slashes.
206     insert(1, ":");  // add a colon after the imputed drive letter.
207 //LOG(astring("turned cygdrive path string into: ") + *this);
208   } else {
209 //LOG(astring("path didn't match so left as: ") + *this);
210   }
211
212   // now we convert msys...
213   if ( (length() >= 2) && (get(0) == DEFAULT_SEPARATOR)
214         && textual::parser_bits::is_alpha(get(1)) ) {
215     // we seem reasonably sure now that this is a windows path hiding in msys format, but
216     // the next character needs to be a slash (if there is a next character) for it to be
217     // the windows drive form.  otherwise it could be /tmp, which would obviously not be
218     // intended as a windows path.
219     if ( (length() == 2) || (get(2) == DEFAULT_SEPARATOR) ) {
220       // cool, this should be interpretable as an msys path, except for those wacky types
221       // of folks that might use a top-level single character directory name.  we cannot
222       // help them, because we have made a design decision to support msys-style paths.
223       // note that this would only affect someone if they were referring to their directory on
224       // the current windows partition (c:, d:, etc.) without providing the drive letter,
225       // if they had that single character directory name (e.g., c:\x, d:\q, etc.) and even
226       // then only on the near defunct windows platform.
227       zap(0, 0);  // take off initial slash.
228       insert(1, ":");  // add the obligatory colon.
229 //LOG(astring("turned msys string into: ") + *this);
230     }
231   } 
232
233   // if we still have a unix style path here on windows, then there will be
234   // trouble when we pass that to the OS.  we are not using any cygwin or
235   // other virtualization libraries directly, so we can't rely on those to
236   // fix the path.  but if we built under something like cygwin, we should
237   // have stored the real dos-style location of the virtual unix root.  we
238   // will use that to replace the root '/' and this should fix most of that
239   // style of path.
240   bool inject_root = false;  // assume we don't need to do anything.
241
242 LOG(astring("before root injection: ") + raw());
243
244   // condition here just checks if the path is only the root.
245   if ( (length() == 1) && separator(get(0)) ) { inject_root = true; }
246
247 if (inject_root) LOG("decided to inject root since path is '/'.");
248
249   // condition is looking for first character being a slash, and second char as alphanumeric or dash or underscore.
250   // we will currently fail detecting freaky paths that don't start off with alphanumeric or one of that small set of special chars.
251   if ( (length() >= 2)
252       && separator(get(0)) 
253       && ( textual::parser_bits::is_alphanumeric(get(1)) || ('-' == get(1)) || ('_' == get(1)) ) ) { 
254     inject_root = true;
255 if (inject_root) LOG(astring("decided to inject root since path is compatible: ") + *this);
256   }
257
258 LOG(astring("after second phase root injection: ") + raw());
259
260   if (inject_root) {
261     // inject the actual path to the unix root in front, if we know it.
262     // if we don't know it, then a default path that's unlikely to work is idiotically plugged in.
263     insert(0, FEISTY_MEOW_VIRTUAL_UNIX_ROOT);
264 ///nope configuration::application_configuration::get_virtual_unix_root());
265 LOG(astring("turned cygdrive path string into: ") + raw());
266   }
267 #endif
268
269   // we don't crop the last separator if the name's too small.  for msdos
270   // names, that would be chopping a slash off the c:\ style name.
271   if (length() > 3) {
272     // zap any separators that are hiding on the end.
273     const int last = end();
274     if (separator(get(last))) zap(last, last);
275   } else if ( (length() == 2) && (get(1) == ':') ) {
276     // special case for dos drive names.  we turn it back into a valid
277     // directory rather than leaving it as just "X:".  that form of the name
278     // means something else under dos/windows.
279     *this += astring(DEFAULT_SEPARATOR, 1);
280   }
281 }
282
283 char filename::drive(bool interact_with_fs) const
284 {
285   // first guess: if second letter's a colon, first letter's the drive.
286   if (length() < 2)
287     return '\0';
288   if (get(1) == ':')
289     return get(0);
290   if (!interact_with_fs)
291     return '\0';
292
293   // otherwise, retrieve the file system's record for the file.
294   status_info fill;
295   if (!get_info(&fill))
296     return '\0';
297   return char('A' + fill.st_dev);
298 }
299
300 astring filename::extension() const
301 {
302   astring base(basename().raw());
303   int posn = base.find('.', base.end(), true);
304   if (negative(posn))
305     return "";
306   return base.substring(posn + 1, base.length() - 1);
307 }
308
309 astring filename::rootname() const
310 {
311   astring base(basename().raw());
312   int posn = base.find('.', base.end(), true);
313   if (negative(posn))
314     return base;
315   return base.substring(0, posn - 1);
316 }
317
318 bool filename::get_info(status_info *to_fill) const
319 {
320   int ret = stat(observe(), to_fill);
321   if (ret)
322     return false;
323   return true;
324 }
325
326 bool filename::is_directory() const
327 {
328   status_info fill;
329   if (!get_info(&fill))
330     return false;
331   return !!(fill.st_mode & S_IFDIR);
332 }
333
334 bool filename::is_writable() const
335 {
336   status_info fill;
337   if (!get_info(&fill))
338     return false;
339   return !!(fill.st_mode & S_IWRITE);
340 }
341
342 bool filename::is_readable() const
343 {
344   status_info fill;
345   if (!get_info(&fill))
346     return false;
347   return !!(fill.st_mode & S_IREAD);
348 }
349
350 bool filename::is_executable() const
351 {
352   status_info fill;
353   if (!get_info(&fill))
354     return false;
355   return !!(fill.st_mode & S_IEXEC);
356 }
357
358 bool filename::is_normal() const
359 {
360   status_info fill;
361   if (!get_info(&fill))
362     return false;
363 #if defined(__WIN32__) || defined(__VMS__)
364 //hmmm: is there a corresponding set of functions for windows, where applicable?
365   bool weird = false;
366 #else
367   bool weird = S_ISCHR(fill.st_mode)
368       || S_ISBLK(fill.st_mode)
369       || S_ISFIFO(fill.st_mode)
370       || S_ISSOCK(fill.st_mode);
371 #endif
372   return !weird;
373 }
374
375 int filename::find_last_separator(const astring &look_at) const
376 {
377   int last_sep = -1;
378   int sep = 0;
379   while (sep >= 0) {
380     sep = look_at.find(DEFAULT_SEPARATOR, last_sep + 1);
381     if (sep >= 0) last_sep = sep;
382   }
383   return last_sep;
384 }
385
386 filename filename::basename() const
387 {
388   astring basename = *this;
389   int last_sep = find_last_separator(basename);
390   if (last_sep >= 0) basename.zap(0, last_sep);
391   return basename;
392 }
393
394 filename filename::dirname() const
395 {
396   astring dirname = *this;
397   int last_sep = find_last_separator(dirname);
398   // we don't accept ripping off the first slash.
399   if (last_sep >= 1) {
400     // we can rip the slash and suffix off to get the directory name.  however,
401     // this might be in the form X: on windows.  if they want the slash to
402     // remain, they can use the dirname that appends it.
403     dirname.zap(last_sep, dirname.end());
404   } else {
405     if (get(0) == DEFAULT_SEPARATOR) {
406       // handle when we're up at the top of the filesystem.  on unix, once
407       // you hit the root, you can keep going up but you still remain at
408       // the root.  similarly on windoze, if there's no drive name in there.
409       dirname = astring(DEFAULT_SEPARATOR, 1);
410     } else {
411       // there's no slash at all in the filename any more.  we assume that
412       // the directory is the current one, if no other information is
413       // available.  this default is already used by some code.
414       dirname = NO_PARENT_DEFAULT;
415     }
416   }
417   return dirname;
418 }
419
420 astring filename::dirname(bool add_slash) const
421 {
422   astring tempname = dirname().raw();
423   if (add_slash) tempname += DEFAULT_SEPARATOR;
424   return tempname;
425 }
426
427 bool filename::exists() const
428 {
429   if (is_directory())
430     return true;
431   // if the file name is empty, that cannot exist.
432   if (!length())
433     return false;
434   return is_readable();
435 }
436
437 bool filename::legal_character(char to_check)
438 {
439   switch (to_check) {
440     case ':': case ';':
441     case '\\': case '/':
442     case '*': case '?': case '$': case '&': case '|':
443     case '\'': case '"': case '`':
444     case '(': case ')':
445     case '[': case ']':
446     case '<': case '>':
447     case '{': case '}':
448       return false;
449     default: return true;
450   }
451 }
452
453 void filename::detooth_filename(astring &to_clean, char replacement)
454 {
455   for (int i = 0; i < to_clean.length(); i++) {
456     if (!legal_character(to_clean[i]))
457       to_clean[i] = replacement;
458   }
459 }
460
461 int filename::packed_size() const
462 {
463   return PACKED_SIZE_INT32 + astring::packed_size();
464 }
465
466 void filename::pack(byte_array &packed_form) const
467 {
468   attach(packed_form, int(_had_directory));
469   astring::pack(packed_form);
470 }
471
472 bool filename::unpack(byte_array &packed_form)
473 {
474   int temp;
475   if (!detach(packed_form, temp))
476     return false;
477   _had_directory = temp;
478   if (!astring::unpack(packed_form))
479     return false;
480   return true;
481 }
482
483 void filename::separate(bool &rooted, string_array &pieces) const
484 {
485   pieces.reset();
486   const astring &raw_form = raw();
487   astring accumulator;  // holds the names we find.
488   rooted = raw_form.length() && separator(raw_form[0]);
489   for (int i = 0; i < raw_form.length(); i++) {
490     if (separator(raw_form[i])) {
491       // this is a separator character, so eat it and add the accumulated
492       // string to the list.
493       if (i && accumulator.length()) pieces += accumulator;
494       // now reset our accumulated text.
495       accumulator = astring::empty_string();
496     } else {
497       // not a separator, so just accumulate it.
498       accumulator += raw_form[i];
499     }
500   }
501   if (accumulator.length()) pieces += accumulator;
502 }
503
504 void filename::join(bool rooted, const string_array &pieces)
505 {
506   astring constructed_name;  // we'll make a filename here.
507   if (rooted) constructed_name += DEFAULT_SEPARATOR;
508   for (int i = 0; i < pieces.length(); i++) {
509     constructed_name += pieces[i];
510     if (!i || (i != pieces.length() - 1))
511       constructed_name += DEFAULT_SEPARATOR;
512   }
513   *this = constructed_name;
514 }
515
516 bool filename::base_compare_prefix(const filename &to_compare,
517     string_array &first, string_array &second)
518 {
519   bool first_rooted;
520   separate(first_rooted, first);
521   bool second_rooted;
522   to_compare.separate(second_rooted, second);
523   if (first_rooted != second_rooted) {
524     return false;
525   }
526   // that case should never be allowed, since there are some bits missing
527   // in the name to be compared.
528   if (first.length() > second.length())
529     return false;
530
531   // compare each of the pieces.
532   for (int i = 0; i < first.length(); i++) {
533 #if defined(__WIN32__) || defined(__VMS__)
534     // case-insensitive compare.
535     if (!first[i].iequals(second[i]))
536       return false;
537 #else
538     // case-sensitive compare.
539     if (first[i] != second[i])
540       return false;
541 #endif
542   }
543   return true;
544 }
545
546 bool filename::compare_prefix(const filename &to_compare, astring &sequel)
547 {
548   sequel = astring::empty_string();  // clean our output parameter.
549   string_array first;
550   string_array second;
551   if (!base_compare_prefix(to_compare, first, second))
552     return false;
553
554   // create the sequel string.
555   int extra_strings = second.length() - first.length();
556   for (int i = second.length() - extra_strings; i < second.length(); i++) {
557     sequel += second[i];
558     if (i != second.length() - 1) sequel += DEFAULT_SEPARATOR;
559   }
560
561   return true;
562 }
563
564 bool filename::compare_prefix(const filename &to_compare)
565 {
566   string_array first;
567   string_array second;
568   return base_compare_prefix(to_compare, first, second);
569 }
570
571 bool filename::base_compare_suffix(const filename &to_compare,
572     string_array &first, string_array &second)
573 {
574   bool first_rooted;
575   separate(first_rooted, first);
576   bool second_rooted;
577   to_compare.separate(second_rooted, second);
578   // that case should never be allowed, since there are some bits missing
579   // in the name to be compared.
580   if (first.length() > second.length())
581     return false;
582
583   // compare each of the pieces.
584   for (int i = first.length() - 1; i >= 0; i--) {
585 //clean up this computation; the difference in lengths is constant--use that.
586     int distance_from_end = first.length() - 1 - i;
587     int j = second.length() - 1 - distance_from_end;
588 #if defined(__WIN32__) || defined(__VMS__)
589     // case-insensitive compare.
590     if (!first[i].iequals(second[j]))
591       return false;
592 #else
593     // case-sensitive compare.
594     if (first[i] != second[j])
595       return false;
596 #endif
597   }
598   return true;
599 }
600
601 bool filename::compare_suffix(const filename &to_compare, astring &prequel)
602 {
603   prequel = astring::empty_string();  // clean our output parameter.
604   string_array first;
605   string_array second;
606   if (!base_compare_suffix(to_compare, first, second))
607     return false;
608
609   // create the prequel string.
610   int extra_strings = second.length() - first.length();
611   for (int i = 0; i < extra_strings; i++) {
612     prequel += second[i];
613     if (i != second.length() - 1) prequel += DEFAULT_SEPARATOR;
614   }
615   return true;
616 }
617
618 bool filename::compare_suffix(const filename &to_compare)
619 {
620   string_array first;
621   string_array second;
622   return base_compare_suffix(to_compare, first, second);
623 }
624
625 bool filename::chmod(int write_mode, int owner_mode) const
626 {
627   int chmod_value = 0;
628 #ifdef __UNIX__
629   if (write_mode & ALLOW_READ) {
630     if (owner_mode & USER_RIGHTS) chmod_value |= S_IRUSR;
631     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IRGRP;
632     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IROTH;
633   }
634   if (write_mode & ALLOW_WRITE) {
635     if (owner_mode & USER_RIGHTS) chmod_value |= S_IWUSR;
636     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IWGRP;
637     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IWOTH;
638   }
639 ////  chmod_value = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
640 #elif defined(__WIN32__)
641   if (write_mode & ALLOW_READ) {
642     chmod_value |= _S_IREAD;
643   }
644   if (write_mode & ALLOW_WRITE) {
645     chmod_value |= _S_IWRITE;
646   }
647 #else
648   #error unsupported OS type currently.
649 #endif
650   int chmod_result = ::chmod(raw().s(), chmod_value);
651   if (chmod_result) {
652 //    LOG(astring("there was a problem changing permissions on ") + raw());
653     return false;
654   }
655   return true;
656 }
657
658 } //namespace.
659