first check-in of feisty meow codebase. many things broken still due to recent
[feisty_meow.git] / core / 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
23 #include <stdio.h>
24 #include <sys/stat.h>
25 #include <sys/types.h>
26 #ifdef __UNIX__
27   #include <unistd.h>
28 #endif
29 #ifdef __WIN32__
30   #include <io.h>
31 #endif
32
33 using namespace basis;
34 using namespace structures;
35
36 class status_info : public stat
37 {
38 };
39
40 namespace filesystem {
41
42 #if defined(__WIN32__) || defined(__VMS__)
43   const char DEFAULT_SEPARATOR = '\\';
44 #elif defined(__UNIX__)
45   const char DEFAULT_SEPARATOR = '/';
46 #else
47   #error "We have no idea what the default path separator is."
48 #endif
49
50 const char *NO_PARENT_DEFAULT = ".";
51   // used when no directory name can be popped off.
52
53 filename::filename()
54 : astring(),
55   _had_directory(false)
56 {}
57
58 filename::filename(const astring &name)
59 : astring(name),
60   _had_directory(true)
61 { canonicalize(); }
62
63 filename::filename(const astring &directory, const astring &name_of_file)
64 : astring(directory),
65   _had_directory(true)
66 {
67   // if the directory is empty, use the current directory.
68   if (!directory) {
69     *this = astring(NO_PARENT_DEFAULT);
70     _had_directory = false;
71   }
72   // check for a slash on the end of the directory.  add one if there is none
73   // currently.
74   bool add_slash = false;
75   if ( (directory[directory.end()] != '\\')
76        && (directory[directory.end()] != '/') ) add_slash = true;
77   if (add_slash) *this += DEFAULT_SEPARATOR;
78   *this += name_of_file;
79   canonicalize();
80 }
81
82 filename::filename(const filename &to_copy)
83 : astring(to_copy),
84   _had_directory(to_copy._had_directory)
85 { canonicalize(); }
86
87 filename::~filename() {}
88
89 astring filename::default_separator() { return astring(DEFAULT_SEPARATOR, 1); }
90
91 astring &filename::raw() { return *this; }
92
93 const astring &filename::raw() const { return *this; }
94
95 bool filename::good() const { return exists(); }
96
97 bool filename::unlink() const { return ::unlink(observe()) == 0; }
98
99 astring filename::null_device()
100 {
101 #ifdef __WIN32__
102   return "null:";
103 #else
104   return "/dev/null";
105 #endif
106 }
107
108 bool filename::separator(char is_it)
109 { return (is_it == pc_separator) || (is_it == unix_separator); }
110
111 filename &filename::operator = (const filename &to_copy)
112 {
113   if (this == &to_copy) return *this;
114   (astring &)(*this) = to_copy;
115   _had_directory = to_copy._had_directory;
116   return *this;
117 }
118
119 filename &filename::operator = (const astring &to_copy)
120 {
121   _had_directory = true;
122   if (this == &to_copy) return *this;
123   (astring &)(*this) = to_copy;
124   canonicalize();
125   return *this;
126 }
127
128 astring filename::pop()
129 {
130   astring to_return = basename();
131   filename parent_dir = parent();
132   if (parent_dir.raw().equal_to(NO_PARENT_DEFAULT)) {
133     // we haven't gone anywhere.
134     return "";  // signal that nothing was removed.
135   }
136   *this = parent_dir;
137   return to_return;
138 }
139
140 filename filename::parent() const { return dirname(); }
141
142 void filename::push(const astring &to_push)
143 {
144   *this = filename(*this, to_push);
145 }
146
147 void filename::canonicalize()
148 {
149   // turn all the non-default separators into the default.
150   bool found_sep = false;
151   for (int j = 0; j < length(); j++) {
152     if (separator(get(j))) {
153       found_sep = true;
154       put(j, DEFAULT_SEPARATOR);
155     }
156   }
157
158   // if there wasn't a single directory separator, then they must not have
159   // specified any directory name for this filename (although it could itself
160   // be a directory).
161   if (!found_sep) _had_directory = false;
162
163   // remove all occurrences of double separators except for the first
164   // double set, which could be a UNC filename.  that's why the index below
165   // starts at one rather than zero.
166   bool saw_sep = false;
167   for (int i = 1; i < length(); i++) {
168     if (separator(get(i))) {
169       if (saw_sep) {
170         zap(i, i);
171           // two in a row is no good, except for the first two.
172         i--;  // skip back one and try again.
173         continue;
174       }
175       saw_sep = true;
176     } else saw_sep = false;
177   }
178
179   // we don't crop the last separator if the name's too small.  for msdos
180   // names, that would be chopping a slash off the c:\ style name.
181   if (length() > 3) {
182     // zap any separators that are hiding on the end.
183     const int last = end();
184     if (separator(get(last))) zap(last, last);
185   } else if ( (length() == 2) && (get(1) == ':') ) {
186     // special case for dos drive names.  we turn it back into a valid
187     // directory rather than leaving it as just "X:".  that form of the name
188     // means something else under dos/windows.
189     *this += astring(DEFAULT_SEPARATOR, 1);
190   }
191 }
192
193 char filename::drive(bool interact_with_fs) const
194 {
195   // first guess: if second letter's a colon, first letter's the drive.
196   if (length() < 2)
197     return '\0';
198   if (get(1) == ':')
199     return get(0);
200   if (!interact_with_fs)
201     return '\0';
202
203   // otherwise, retrieve the file system's record for the file.
204   status_info fill;
205   if (!get_info(&fill))
206     return '\0';
207   return char('A' + fill.st_dev);
208 }
209
210 astring filename::extension() const
211 {
212   astring base(basename().raw());
213   int posn = base.find('.', base.end(), true);
214   if (negative(posn))
215     return "";
216   return base.substring(posn + 1, base.length() - 1);
217 }
218
219 astring filename::rootname() const
220 {
221   astring base(basename().raw());
222   int posn = base.find('.', base.end(), true);
223   if (negative(posn))
224     return base;
225   return base.substring(0, posn - 1);
226 }
227
228 bool filename::get_info(status_info *to_fill) const
229 {
230   int ret = stat(observe(), to_fill);
231   if (ret)
232     return false;
233   return true;
234 }
235
236 bool filename::is_directory() const
237 {
238   status_info fill;
239   if (!get_info(&fill))
240     return false;
241   return !!(fill.st_mode & S_IFDIR);
242 }
243
244 bool filename::is_writable() const
245 {
246   status_info fill;
247   if (!get_info(&fill))
248     return false;
249   return !!(fill.st_mode & S_IWRITE);
250 }
251
252 bool filename::is_readable() const
253 {
254   status_info fill;
255   if (!get_info(&fill))
256     return false;
257   return !!(fill.st_mode & S_IREAD);
258 }
259
260 bool filename::is_executable() const
261 {
262   status_info fill;
263   if (!get_info(&fill))
264     return false;
265   return !!(fill.st_mode & S_IEXEC);
266 }
267
268 int filename::find_last_separator(const astring &look_at) const
269 {
270   int last_sep = -1;
271   int sep = 0;
272   while (sep >= 0) {
273     sep = look_at.find(DEFAULT_SEPARATOR, last_sep + 1);
274     if (sep >= 0) last_sep = sep;
275   }
276   return last_sep;
277 }
278
279 filename filename::basename() const
280 {
281   astring basename = *this;
282   int last_sep = find_last_separator(basename);
283   if (last_sep >= 0) basename.zap(0, last_sep);
284   return basename;
285 }
286
287 filename filename::dirname() const
288 {
289   astring dirname = *this;
290   int last_sep = find_last_separator(dirname);
291   // we don't accept ripping off the first slash.
292   if (last_sep >= 1) {
293     // we can rip the slash and suffix off to get the directory name.  however,
294     // this might be in the form X: on windows.  if they want the slash to
295     // remain, they can use the dirname that appends it.
296     dirname.zap(last_sep, dirname.end());
297   } else {
298     if (get(0) == DEFAULT_SEPARATOR) {
299       // handle when we're up at the top of the filesystem.  on unix, once
300       // you hit the root, you can keep going up but you still remain at
301       // the root.  similarly on windoze, if there's no drive name in there.
302       dirname = astring(DEFAULT_SEPARATOR, 1);
303     } else {
304       // there's no slash at all in the filename any more.  we assume that
305       // the directory is the current one, if no other information is
306       // available.  this default is already used by some code.
307       dirname = NO_PARENT_DEFAULT;
308     }
309   }
310   return dirname;
311 }
312
313 astring filename::dirname(bool add_slash) const
314 {
315   astring tempname = dirname().raw();
316   if (add_slash) tempname += DEFAULT_SEPARATOR;
317   return tempname;
318 }
319
320 bool filename::exists() const
321 {
322   if (is_directory())
323     return true;
324   if (!length())
325     return false;
326   return is_readable();
327 ///  byte_filer opened(observe(), "rb");
328 ///  return opened.good();
329 }
330
331 bool filename::legal_character(char to_check)
332 {
333   switch (to_check) {
334     case ':': case ';':
335     case '\\': case '/':
336     case '*': case '?': case '$': case '&': case '|':
337     case '\'': case '"': case '`':
338     case '(': case ')':
339     case '[': case ']':
340     case '<': case '>':
341     case '{': case '}':
342       return false;
343     default: return true;
344   }
345 }
346
347 void filename::detooth_filename(astring &to_clean, char replacement)
348 {
349   for (int i = 0; i < to_clean.length(); i++) {
350     if (!legal_character(to_clean[i]))
351       to_clean[i] = replacement;
352   }
353 }
354
355 int filename::packed_size() const
356 {
357   return PACKED_SIZE_INT32 + astring::packed_size();
358 }
359
360 void filename::pack(byte_array &packed_form) const
361 {
362   attach(packed_form, int(_had_directory));
363   astring::pack(packed_form);
364 }
365
366 bool filename::unpack(byte_array &packed_form)
367 {
368   int temp;
369   if (!detach(packed_form, temp))
370     return false;
371   _had_directory = temp;
372   if (!astring::unpack(packed_form))
373     return false;
374   return true;
375 }
376
377 void filename::separate(string_array &pieces) const
378 {
379   pieces.reset();
380   const astring &raw_form = raw();
381   astring accumulator;  // holds the names we find.
382   for (int i = 0; i < raw_form.length(); i++) {
383     if (separator(raw_form[i])) {
384       // this is a separator character, so eat it and add the accumulated
385       // string to the list.
386       if (!i || accumulator.length()) pieces += accumulator;
387       // now reset our accumulated text.
388       accumulator = astring::empty_string();
389     } else {
390       // not a separator, so just accumulate it.
391       accumulator += raw_form[i];
392     }
393   }
394   if (accumulator.length()) pieces += accumulator;
395 }
396
397 void filename::join(const string_array &pieces)
398 {
399   astring constructed_name;  // we'll make a filename here.
400   for (int i = 0; i < pieces.length(); i++) {
401     constructed_name += pieces[i];
402     if (!i || (i != pieces.length() - 1))
403       constructed_name += DEFAULT_SEPARATOR;
404   }
405   *this = constructed_name;
406 }
407
408 bool filename::base_compare_prefix(const filename &to_compare,
409     string_array &first, string_array &second)
410 {
411   separate(first);
412   to_compare.separate(second);
413   // that case should never be allowed, since there are some bits missing
414   // in the name to be compared.
415   if (first.length() > second.length())
416     return false;
417
418   // compare each of the pieces.
419   for (int i = 0; i < first.length(); i++) {
420 #if defined(__WIN32__) || defined(__VMS__)
421     // case-insensitive compare.
422     if (!first[i].iequals(second[i]))
423       return false;
424 #else
425     // case-sensitive compare.
426     if (first[i] != second[i])
427       return false;
428 #endif
429   }
430   return true;
431 }
432
433 bool filename::compare_prefix(const filename &to_compare, astring &sequel)
434 {
435   sequel = astring::empty_string();  // clean our output parameter.
436   string_array first;
437   string_array second;
438   if (!base_compare_prefix(to_compare, first, second))
439     return false;
440
441   // create the sequel string.
442   int extra_strings = second.length() - first.length();
443   for (int i = second.length() - extra_strings; i < second.length(); i++) {
444     sequel += second[i];
445     if (i != second.length() - 1) sequel += DEFAULT_SEPARATOR;
446   }
447
448   return true;
449 }
450
451 bool filename::compare_prefix(const filename &to_compare)
452 {
453   string_array first;
454   string_array second;
455   return base_compare_prefix(to_compare, first, second);
456 }
457
458 bool filename::base_compare_suffix(const filename &to_compare,
459     string_array &first, string_array &second)
460 {
461   separate(first);
462   to_compare.separate(second);
463   // that case should never be allowed, since there are some bits missing
464   // in the name to be compared.
465   if (first.length() > second.length())
466     return false;
467
468   // compare each of the pieces.
469   for (int i = first.length() - 1; i >= 0; i--) {
470 //clean up this computation; the difference in lengths is constant--use that.
471     int distance_from_end = first.length() - 1 - i;
472     int j = second.length() - 1 - distance_from_end;
473 #if defined(__WIN32__) || defined(__VMS__)
474     // case-insensitive compare.
475     if (!first[i].iequals(second[j]))
476       return false;
477 #else
478     // case-sensitive compare.
479     if (first[i] != second[j])
480       return false;
481 #endif
482   }
483   return true;
484 }
485
486 bool filename::compare_suffix(const filename &to_compare, astring &prequel)
487 {
488   prequel = astring::empty_string();  // clean our output parameter.
489   string_array first;
490   string_array second;
491   if (!base_compare_suffix(to_compare, first, second))
492     return false;
493
494   // create the prequel string.
495   int extra_strings = second.length() - first.length();
496   for (int i = 0; i < extra_strings; i++) {
497     prequel += second[i];
498     if (i != second.length() - 1) prequel += DEFAULT_SEPARATOR;
499   }
500   return true;
501 }
502
503 bool filename::compare_suffix(const filename &to_compare)
504 {
505   string_array first;
506   string_array second;
507   return base_compare_suffix(to_compare, first, second);
508 }
509
510 bool filename::chmod(int write_mode, int owner_mode) const
511 {
512   int chmod_value = 0;
513 #ifdef __UNIX__
514   if (write_mode & ALLOW_READ) {
515     if (owner_mode & USER_RIGHTS) chmod_value |= S_IRUSR;
516     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IRGRP;
517     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IROTH;
518   }
519   if (write_mode & ALLOW_WRITE) {
520     if (owner_mode & USER_RIGHTS) chmod_value |= S_IWUSR;
521     if (owner_mode & GROUP_RIGHTS) chmod_value |= S_IWGRP;
522     if (owner_mode & OTHER_RIGHTS) chmod_value |= S_IWOTH;
523   }
524 ////  chmod_value = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
525 #elif defined(__WIN32__)
526   if (write_mode & ALLOW_READ) {
527     chmod_value |= _S_IREAD;
528   }
529   if (write_mode & ALLOW_WRITE) {
530     chmod_value |= _S_IWRITE;
531   }
532 #else
533   #error unsupported OS type currently.
534 #endif
535   int chmod_result = ::chmod(raw().s(), chmod_value);
536   if (chmod_result) {
537 //    LOG(astring("there was a problem changing permissions on ") + raw());
538     return false;
539   }
540   return true;
541 }
542
543 } //namespace.
544