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