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