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