4ea4269aabaff86631452a3212a8e788f1b410cc
[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 (!length())
387     return false;
388   return is_readable();
389 ///  byte_filer opened(observe(), "rb");
390 ///  return opened.good();
391 }
392
393 bool filename::legal_character(char to_check)
394 {
395   switch (to_check) {
396     case ':': case ';':
397     case '\\': case '/':
398     case '*': case '?': case '$': case '&': case '|':
399     case '\'': case '"': case '`':
400     case '(': case ')':
401     case '[': case ']':
402     case '<': case '>':
403     case '{': case '}':
404       return false;
405     default: return true;
406   }
407 }
408
409 void filename::detooth_filename(astring &to_clean, char replacement)
410 {
411   for (int i = 0; i < to_clean.length(); i++) {
412     if (!legal_character(to_clean[i]))
413       to_clean[i] = replacement;
414   }
415 }
416
417 int filename::packed_size() const
418 {
419   return PACKED_SIZE_INT32 + astring::packed_size();
420 }
421
422 void filename::pack(byte_array &packed_form) const
423 {
424   attach(packed_form, int(_had_directory));
425   astring::pack(packed_form);
426 }
427
428 bool filename::unpack(byte_array &packed_form)
429 {
430   int temp;
431   if (!detach(packed_form, temp))
432     return false;
433   _had_directory = temp;
434   if (!astring::unpack(packed_form))
435     return false;
436   return true;
437 }
438
439 void filename::separate(bool &rooted, string_array &pieces) const
440 {
441   pieces.reset();
442   const astring &raw_form = raw();
443   astring accumulator;  // holds the names we find.
444   rooted = raw_form.length() && separator(raw_form[0]);
445   for (int i = 0; i < raw_form.length(); i++) {
446     if (separator(raw_form[i])) {
447       // this is a separator character, so eat it and add the accumulated
448       // string to the list.
449       if (i && accumulator.length()) pieces += accumulator;
450       // now reset our accumulated text.
451       accumulator = astring::empty_string();
452     } else {
453       // not a separator, so just accumulate it.
454       accumulator += raw_form[i];
455     }
456   }
457   if (accumulator.length()) pieces += accumulator;
458 }
459
460 void filename::join(bool rooted, const string_array &pieces)
461 {
462   astring constructed_name;  // we'll make a filename here.
463   if (rooted) constructed_name += DEFAULT_SEPARATOR;
464   for (int i = 0; i < pieces.length(); i++) {
465     constructed_name += pieces[i];
466     if (!i || (i != pieces.length() - 1))
467       constructed_name += DEFAULT_SEPARATOR;
468   }
469   *this = constructed_name;
470 }
471
472 bool filename::base_compare_prefix(const filename &to_compare,
473     string_array &first, string_array &second)
474 {
475   bool first_rooted;
476   separate(first_rooted, first);
477   bool second_rooted;
478   to_compare.separate(second_rooted, second);
479   if (first_rooted != second_rooted) {
480     return false;
481   }
482   // that case should never be allowed, since there are some bits missing
483   // in the name to be compared.
484   if (first.length() > second.length())
485     return false;
486
487   // compare each of the pieces.
488   for (int i = 0; i < first.length(); i++) {
489 #if defined(__WIN32__) || defined(__VMS__)
490     // case-insensitive compare.
491     if (!first[i].iequals(second[i]))
492       return false;
493 #else
494     // case-sensitive compare.
495     if (first[i] != second[i])
496       return false;
497 #endif
498   }
499   return true;
500 }
501
502 bool filename::compare_prefix(const filename &to_compare, astring &sequel)
503 {
504   sequel = astring::empty_string();  // clean our output parameter.
505   string_array first;
506   string_array second;
507   if (!base_compare_prefix(to_compare, first, second))
508     return false;
509
510   // create the sequel string.
511   int extra_strings = second.length() - first.length();
512   for (int i = second.length() - extra_strings; i < second.length(); i++) {
513     sequel += second[i];
514     if (i != second.length() - 1) sequel += DEFAULT_SEPARATOR;
515   }
516
517   return true;
518 }
519
520 bool filename::compare_prefix(const filename &to_compare)
521 {
522   string_array first;
523   string_array second;
524   return base_compare_prefix(to_compare, first, second);
525 }
526
527 bool filename::base_compare_suffix(const filename &to_compare,
528     string_array &first, string_array &second)
529 {
530   bool first_rooted;
531   separate(first_rooted, first);
532   bool second_rooted;
533   to_compare.separate(second_rooted, second);
534   // that case should never be allowed, since there are some bits missing
535   // in the name to be compared.
536   if (first.length() > second.length())
537     return false;
538
539   // compare each of the pieces.
540   for (int i = first.length() - 1; i >= 0; i--) {
541 //clean up this computation; the difference in lengths is constant--use that.
542     int distance_from_end = first.length() - 1 - i;
543     int j = second.length() - 1 - distance_from_end;
544 #if defined(__WIN32__) || defined(__VMS__)
545     // case-insensitive compare.
546     if (!first[i].iequals(second[j]))
547       return false;
548 #else
549     // case-sensitive compare.
550     if (first[i] != second[j])
551       return false;
552 #endif
553   }
554   return true;
555 }
556
557 bool filename::compare_suffix(const filename &to_compare, astring &prequel)
558 {
559   prequel = astring::empty_string();  // clean our output parameter.
560   string_array first;
561   string_array second;
562   if (!base_compare_suffix(to_compare, first, second))
563     return false;
564
565   // create the prequel string.
566   int extra_strings = second.length() - first.length();
567   for (int i = 0; i < extra_strings; i++) {
568     prequel += second[i];
569     if (i != second.length() - 1) prequel += DEFAULT_SEPARATOR;
570   }
571   return true;
572 }
573
574 bool filename::compare_suffix(const filename &to_compare)
575 {
576   string_array first;
577   string_array second;
578   return base_compare_suffix(to_compare, first, second);
579 }
580
581 bool filename::chmod(int write_mode, int owner_mode) const
582 {
583   int chmod_value = 0;
584 #ifdef __UNIX__
585   if (write_mode & ALLOW_READ) {
586     if (owner_mode & USER_RIGHTS) chmod_value |= S_IRUSR;
587     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IRGRP;
588     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IROTH;
589   }
590   if (write_mode & ALLOW_WRITE) {
591     if (owner_mode & USER_RIGHTS) chmod_value |= S_IWUSR;
592     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IWGRP;
593     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IWOTH;
594   }
595 ////  chmod_value = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
596 #elif defined(__WIN32__)
597   if (write_mode & ALLOW_READ) {
598     chmod_value |= _S_IREAD;
599   }
600   if (write_mode & ALLOW_WRITE) {
601     chmod_value |= _S_IWRITE;
602   }
603 #else
604   #error unsupported OS type currently.
605 #endif
606   int chmod_result = ::chmod(raw().s(), chmod_value);
607   if (chmod_result) {
608 //    LOG(astring("there was a problem changing permissions on ") + raw());
609     return false;
610   }
611   return true;
612 }
613
614 } //namespace.
615