monotone

monotone Mtn Source Tree

Root/paths.cc

1// Copyright (C) 2005 Nathaniel Smith <njs@pobox.com>
2//
3// This program is made available under the GNU GPL version 2.0 or
4// greater. See the accompanying file COPYING for details.
5//
6// This program is distributed WITHOUT ANY WARRANTY; without even the
7// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
8// PURPOSE.
9
10#include <string>
11#include <sstream>
12
13#include <boost/filesystem/operations.hpp>
14#include <boost/filesystem/convenience.hpp>
15
16#include "constants.hh"
17#include "paths.hh"
18#include "platform-wrapped.hh"
19#include "sanity.hh"
20#include "interner.hh"
21#include "charset.hh"
22#include "simplestring_xform.hh"
23
24using std::exception;
25using std::ostream;
26using std::ostringstream;
27using std::string;
28using std::vector;
29
30
31// some structure to ensure we aren't doing anything broken when resolving
32// filenames. the idea is to make sure
33// -- we don't depend on the existence of something before it has been set
34// -- we don't re-set something that has already been used
35// -- sometimes, we use the _non_-existence of something, so we shouldn't
36// set anything whose un-setted-ness has already been used
37template <typename T>
38struct access_tracker
39{
40 void set(T const & val, bool may_be_initialized)
41 {
42 I(may_be_initialized || !initialized);
43 I(!very_uninitialized);
44 I(!used);
45 initialized = true;
46 value = val;
47 }
48 T const & get()
49 {
50 I(initialized);
51 used = true;
52 return value;
53 }
54 T const & get_but_unused()
55 {
56 I(initialized);
57 return value;
58 }
59 void may_not_initialize()
60 {
61 I(!initialized);
62 very_uninitialized = true;
63 }
64 // for unit tests
65 void unset()
66 {
67 used = initialized = very_uninitialized = false;
68 }
69 T value;
70 bool initialized, used, very_uninitialized;
71 access_tracker() : initialized(false), used(false), very_uninitialized(false) {};
72};
73
74// paths to use in interpreting paths from various sources,
75// conceptually:
76// working_root / initial_rel_path == initial_abs_path
77
78// initial_abs_path is for interpreting relative system_path's
79static access_tracker<system_path> initial_abs_path;
80// initial_rel_path is for interpreting external file_path's
81// for now we just make it an fs::path for convenience; we used to make it a
82// file_path, but then you can't run monotone from inside the _MTN/ dir (even
83// when referring to files outside the _MTN/ dir).
84static access_tracker<fs::path> initial_rel_path;
85// working_root is for converting file_path's and bookkeeping_path's to
86// system_path's.
87static access_tracker<system_path> working_root;
88
89bookkeeping_path const bookkeeping_root("_MTN");
90path_component const bookkeeping_root_component("_MTN");
91
92// this is a file_path because it does not conform to the invariant that
93// bookkeeping paths always start with the _current_ bookkeeping root.
94file_path const old_bookkeeping_root = file_path_internal("MT");
95
96void
97save_initial_path()
98{
99 // FIXME: BUG: this only works if the current working dir is in utf8
100 initial_abs_path.set(system_path(get_current_working_dir()), false);
101 // We still use boost::fs, so let's continue to initialize it properly.
102 fs::initial_path();
103 fs::path::default_name_check(fs::native);
104 L(FL("initial abs path is: %s") % initial_abs_path.get_but_unused());
105}
106
107///////////////////////////////////////////////////////////////////////////
108// verifying that internal paths are indeed normalized.
109// this code must be superfast
110///////////////////////////////////////////////////////////////////////////
111
112// normalized means:
113// -- / as path separator
114// -- not an absolute path (on either posix or win32)
115// operationally, this means: first character != '/', first character != '\',
116// second character != ':'
117// -- no illegal characters
118// -- 0x00 -- 0x1f, 0x7f, \ are the illegal characters. \ is illegal
119// unconditionally to prevent people checking in files on posix that
120// have a different interpretation on win32
121// -- (may want to allow 0x0a and 0x0d (LF and CR) in the future, but this
122// is blocked on manifest format changing)
123// (also requires changes to 'automate inventory', possibly others, to
124// handle quoting)
125// -- no doubled /'s
126// -- no trailing /
127// -- no "." or ".." path components
128static inline bool
129bad_component(string const & component)
130{
131 static const string dot(".");
132 static const string dotdot("..");
133 if (component.empty())
134 return true;
135 if (component == dot)
136 return true;
137 if (component == dotdot)
138 return true;
139 return false;
140}
141
142static inline bool
143has_bad_chars(string const & path)
144{
145 for (string::const_iterator c = path.begin(); LIKELY(c != path.end()); c++)
146 {
147 // char is often a signed type; convert to unsigned to ensure that
148 // bytes 0x80-0xff are considered > 0x1f.
149 u8 x = (u8)*c;
150 // 0x5c is '\\'; we use the hex constant to make the dependency on
151 // ASCII encoding explicit.
152 if (UNLIKELY(x <= 0x1f || x == 0x5c || x == 0x7f))
153 return true;
154 }
155 return false;
156}
157
158// fully_normalized_path_split performs very similar function to
159// file_path.split(). if want_split is set, split_path will be filled with
160// the '/' separated components of the path.
161static inline bool
162fully_normalized_path_split(string const & path, bool want_split,
163 split_path & sp)
164{
165 // empty path is fine
166 if (path.empty())
167 return true;
168 // could use is_absolute_somewhere, but this is the only part of it that
169 // wouldn't be redundant
170 if (path.size() > 1 && path[1] == ':')
171 return false;
172 // first scan for completely illegal bytes
173 if (has_bad_chars(path))
174 return false;
175 // now check each component
176 string::size_type start, stop;
177 start = 0;
178 while (1)
179 {
180 stop = path.find('/', start);
181 if (stop == string::npos)
182 {
183 string const & s(path.substr(start));
184 if (bad_component(s))
185 return false;
186 if (want_split)
187 sp.push_back(path_component(s));
188 break;
189 }
190 string const & s(path.substr(start, stop - start));
191 if (bad_component(s))
192 return false;
193 if (want_split)
194 sp.push_back(path_component(s));
195 start = stop + 1;
196 }
197 return true;
198}
199
200static inline bool
201fully_normalized_path(string const & path)
202{
203 split_path sp;
204 return fully_normalized_path_split(path, false, sp);
205}
206
207// This function considers _MTN, _MTn, _MtN, _mtn etc. to all be bookkeeping
208// paths, because on case insensitive filesystems, files put in any of them
209// may end up in _MTN instead. This allows arbitrary code execution. A
210// better solution would be to fix this in the working directory writing code
211// -- this prevents all-unix projects from naming things "mt", which is a bit
212// rude -- but as a temporary security kluge it works.
213static inline bool
214in_bookkeeping_dir(string const & path)
215{
216 if (path.size() == 0 || (path[0] != '_'))
217 return false;
218 if (path.size() == 1 || (path[1] != 'M' && path[1] != 'm'))
219 return false;
220 if (path.size() == 2 || (path[2] != 'T' && path[2] != 't'))
221 return false;
222 if (path.size() == 3 || (path[3] != 'N' && path[3] != 'n'))
223 return false;
224 // if we've gotten here, the first three letters are _, M, T, and N, in
225 // either upper or lower case. So if that is the whole path, or else if it
226 // continues but the next character is /, then this is a bookkeeping path.
227 if (path.size() == 4 || (path[4] == '/'))
228 return true;
229 return false;
230}
231
232static inline bool
233is_valid_internal(string const & path)
234{
235 return (fully_normalized_path(path)
236 && !in_bookkeeping_dir(path));
237}
238
239// equivalent to file_path_internal(path).split(sp), but
240// avoids splitting the string twice
241void
242internal_string_to_split_path(string const & path, split_path & sp)
243{
244 I(utf8_validate(utf8(path)));
245 I(!in_bookkeeping_dir(path));
246 sp.clear();
247 sp.reserve(8);
248 sp.push_back(the_null_component);
249 I(fully_normalized_path_split(path, true, sp));
250}
251
252static void
253normalize_external_path(string const & path, string & normalized)
254{
255 if (!initial_rel_path.initialized)
256 {
257 // we are not in a workspace; treat this as an internal
258 // path, and set the access_tracker() into a very uninitialised
259 // state so that we will hit an exception if we do eventually
260 // enter a workspace
261 initial_rel_path.may_not_initialize();
262 normalized = path;
263 N(is_valid_internal(path),
264 F("path '%s' is invalid") % path);
265 }
266 else
267 {
268 N(!path.empty(), F("empty path '%s' is invalid") % path);
269 fs::path out, base, relative;
270 try
271 {
272 base = initial_rel_path.get();
273 // the fs::native is needed to get it to accept paths like ".foo".
274 relative = fs::path(path, fs::native);
275 out = (base / relative).normalize();
276 }
277 catch (exception &)
278 {
279 N(false, F("path '%s' is invalid") % path);
280 }
281 normalized = out.string();
282 if (normalized == ".")
283 normalized = string("");
284 N(!relative.has_root_path(),
285 F("absolute path '%s' is invalid") % relative.string());
286 N(fully_normalized_path(normalized), F("path '%s' is invalid") % normalized);
287 }
288}
289
290file_path::file_path(file_path::source_type type, string const & path)
291{
292 string normalized;
293 MM(path);
294 I(utf8_validate(utf8(path)));
295 switch (type)
296 {
297 case internal:
298 data = utf8(path);
299 break;
300 case external:
301 normalize_external_path(path, normalized);
302 data = utf8(normalized);
303 N(!in_bookkeeping_dir(data()), F("path '%s' is in bookkeeping dir") % data);
304 break;
305 }
306 MM(data);
307 I(is_valid_internal(data()));
308}
309
310bookkeeping_path::bookkeeping_path(string const & path)
311{
312 I(fully_normalized_path(path));
313 I(in_bookkeeping_dir(path));
314 data = utf8(path);
315}
316
317bool
318bookkeeping_path::external_string_is_bookkeeping_path(utf8 const & path)
319{
320 // FIXME: this charset casting everywhere is ridiculous
321 string normalized;
322 normalize_external_path(path(), normalized);
323 return internal_string_is_bookkeeping_path(utf8(normalized));
324}
325bool bookkeeping_path::internal_string_is_bookkeeping_path(utf8 const & path)
326{
327 return in_bookkeeping_dir(path());
328}
329
330///////////////////////////////////////////////////////////////////////////
331// splitting/joining
332// this code must be superfast
333// it depends very much on knowing that it can only be applied to fully
334// normalized, relative, paths.
335///////////////////////////////////////////////////////////////////////////
336
337// This function takes a vector of path components and joins them into a
338// single file_path. This is the inverse to file_path::split. It takes a
339// vector of the form:
340//
341// ["", p[0], p[1], ..., p[n]]
342//
343// and constructs the path:
344//
345// p[0]/p[1]/.../p[n]
346//
347file_path::file_path(split_path const & sp)
348{
349 split_path::const_iterator i = sp.begin();
350 I(i != sp.end());
351 I(null_name(*i));
352 string tmp;
353 bool start = true;
354 for (++i; i != sp.end(); ++i)
355 {
356 I(!null_name(*i));
357 if (!start)
358 tmp += "/";
359 tmp += (*i)();
360 if (start)
361 start = false;
362 }
363 I(!in_bookkeeping_dir(tmp));
364 data = utf8(tmp);
365}
366
367//
368// this takes a path of the form
369//
370// "p[0]/p[1]/.../p[n-1]/p[n]"
371//
372// and fills in a vector of paths corresponding to p[0] ... p[n]. This is the
373// inverse to the file_path::file_path(split_path) constructor.
374//
375// The first entry in this vector is always the null component, "". This path
376// is the root of the tree. So we actually output a vector like:
377// ["", p[0], p[1], ..., p[n]]
378// with n+1 members.
379void
380file_path::split(split_path & sp) const
381{
382 sp.clear();
383 sp.push_back(the_null_component);
384 if (empty())
385 return;
386 string::size_type start, stop;
387 start = 0;
388 string const & s = data();
389 while (1)
390 {
391 stop = s.find('/', start);
392 if (stop == string::npos)
393 {
394 sp.push_back(path_component(s.substr(start)));
395 break;
396 }
397 sp.push_back(path_component(s.substr(start, stop - start)));
398 start = stop + 1;
399 }
400}
401
402void
403split_paths(std::vector<file_path> const & file_paths, path_set & split_paths)
404{
405 for (vector<file_path>::const_iterator i = file_paths.begin();
406 i != file_paths.end(); ++i)
407 {
408 split_path sp;
409 i->split(sp);
410 split_paths.insert(sp);
411 }
412}
413
414template <>
415void dump(split_path const & sp, string & out)
416{
417 ostringstream oss;
418
419 for (split_path::const_iterator i = sp.begin(); i != sp.end(); ++i)
420 {
421 if (null_name(*i))
422 oss << '.';
423 else
424 oss << '/' << *i;
425 }
426
427 oss << '\n';
428
429 out = oss.str();
430}
431
432
433///////////////////////////////////////////////////////////////////////////
434// localizing file names (externalizing them)
435// this code must be superfast when there is no conversion needed
436///////////////////////////////////////////////////////////////////////////
437
438string
439any_path::as_external() const
440{
441#ifdef __APPLE__
442 // on OS X paths for the filesystem/kernel are UTF-8 encoded, regardless of
443 // locale.
444 return data();
445#else
446 // on normal systems we actually have some work to do, alas.
447 // not much, though, because utf8_to_system_string does all the hard work.
448 // it is carefully optimized. do not screw it up.
449 external out;
450 utf8_to_system_strict(data, out);
451 return out();
452#endif
453}
454
455///////////////////////////////////////////////////////////////////////////
456// writing out paths
457///////////////////////////////////////////////////////////////////////////
458
459ostream &
460operator <<(ostream & o, any_path const & a)
461{
462 o << a.as_internal();
463 return o;
464}
465
466ostream &
467operator <<(ostream & o, split_path const & sp)
468{
469 file_path tmp(sp);
470 return o << tmp;
471}
472
473///////////////////////////////////////////////////////////////////////////
474// path manipulation
475// this code's speed does not matter much
476///////////////////////////////////////////////////////////////////////////
477
478static bool
479is_absolute_here(string const & path)
480{
481 if (path.empty())
482 return false;
483 if (path[0] == '/')
484 return true;
485#ifdef WIN32
486 if (path[0] == '\\')
487 return true;
488 if (path.size() > 1 && path[1] == ':')
489 return true;
490#endif
491 return false;
492}
493
494static inline bool
495is_absolute_somewhere(string const & path)
496{
497 if (path.empty())
498 return false;
499 if (path[0] == '/')
500 return true;
501 if (path[0] == '\\')
502 return true;
503 if (path.size() > 1 && path[1] == ':')
504 return true;
505 return false;
506}
507
508file_path
509file_path::operator /(string const & to_append) const
510{
511 I(!is_absolute_somewhere(to_append));
512 if (empty())
513 return file_path_internal(to_append);
514 else
515 return file_path_internal(data() + "/" + to_append);
516}
517
518bookkeeping_path
519bookkeeping_path::operator /(string const & to_append) const
520{
521 I(!is_absolute_somewhere(to_append));
522 I(!empty());
523 return bookkeeping_path(data() + "/" + to_append);
524}
525
526system_path
527system_path::operator /(string const & to_append) const
528{
529 I(!empty());
530 I(!is_absolute_here(to_append));
531 return system_path(data() + "/" + to_append);
532}
533
534///////////////////////////////////////////////////////////////////////////
535// system_path
536///////////////////////////////////////////////////////////////////////////
537
538static string
539normalize_out_dots(string const & path)
540{
541#ifdef WIN32
542 return fs::path(path, fs::native).normalize().string();
543#else
544 return fs::path(path, fs::native).normalize().native_file_string();
545#endif
546}
547
548system_path::system_path(any_path const & other, bool in_true_workspace)
549{
550 if (is_absolute_here(other.as_internal()))
551 // another system_path. the normalizing isn't really necessary, but it
552 // makes me feel warm and fuzzy.
553 data = utf8(normalize_out_dots(other.as_internal()));
554 else
555 {
556 system_path wr;
557 if (in_true_workspace)
558 wr = working_root.get();
559 else
560 wr = working_root.get_but_unused();
561 data = utf8(normalize_out_dots((wr / other.as_internal()).as_internal()));
562 }
563}
564
565static inline string const_system_path(utf8 const & path)
566{
567 N(!path().empty(), F("invalid path ''"));
568 string expanded = tilde_expand(path)();
569 if (is_absolute_here(expanded))
570 return normalize_out_dots(expanded);
571 else
572 return normalize_out_dots((initial_abs_path.get() / expanded).as_internal());
573}
574
575system_path::system_path(string const & path)
576{
577 data = utf8(const_system_path(utf8(path)));
578}
579
580system_path::system_path(utf8 const & path)
581{
582 data = utf8(const_system_path(utf8(path)));
583}
584
585///////////////////////////////////////////////////////////////////////////
586// utility
587///////////////////////////////////////////////////////////////////////////
588
589bool
590workspace_root(split_path const & sp)
591{
592 I(null_name(idx(sp,0)));
593 return sp.size() == 1;
594}
595
596void
597dirname_basename(split_path const & sp,
598 split_path & dirname, path_component & basename)
599{
600 I(!sp.empty());
601 // L(FL("dirname_basename('%s' [%d components],...)") % file_path(sp) % sp.size());
602 dirname = sp;
603 dirname.pop_back();
604 basename = sp.back();
605 if (dirname.empty())
606 {
607 // L(FL("basename %d vs. null component %d") % basename % the_null_component);
608 I(null_name(basename));
609 }
610}
611
612///////////////////////////////////////////////////////////////////////////
613// workspace (and path root) handling
614///////////////////////////////////////////////////////////////////////////
615
616system_path
617current_root_path()
618{
619 return system_path(fs::initial_path().root_path().string());
620}
621
622static bool
623find_bookdir(fs::path const & root, fs::path const & bookdir,
624 fs::path & current, fs::path & removed)
625{
626 current = fs::initial_path();
627 fs::path check = current / bookdir;
628
629 // check that the current directory is below the specified search root
630
631 fs::path::iterator ri = root.begin();
632 fs::path::iterator ci = current.begin();
633
634 while (ri != root.end() && ci != current.end() && *ri == *ci)
635 {
636 ++ri;
637 ++ci;
638 }
639
640 // if it's not then issue a warning and abort the search
641
642 if (ri != root.end())
643 {
644 W(F("current directory '%s' is not below root '%s'")
645 % current.string()
646 % root.string());
647 return false;
648 }
649
650 L(FL("searching for '%s' directory with root '%s'")
651 % bookdir.string()
652 % root.string());
653
654 while (current != root
655 && current.has_branch_path()
656 && current.has_leaf()
657 && !fs::exists(check))
658 {
659 L(FL("'%s' not found in '%s' with '%s' removed")
660 % bookdir.string() % current.string() % removed.string());
661 removed = fs::path(current.leaf(), fs::native) / removed;
662 current = current.branch_path();
663 check = current / bookdir;
664 }
665
666 L(FL("search for '%s' ended at '%s' with '%s' removed")
667 % bookdir.string() % current.string() % removed.string());
668
669 if (!fs::exists(check))
670 {
671 L(FL("'%s' does not exist") % check.string());
672 return false;
673 }
674
675 if (!fs::is_directory(check))
676 {
677 L(FL("'%s' is not a directory") % check.string());
678 return false;
679 }
680
681 // check for _MTN/. and _MTN/.. to see if mt dir is readable
682 if (!fs::exists(check / ".") || !fs::exists(check / ".."))
683 {
684 L(FL("problems with '%s' (missing '.' or '..')") % check.string());
685 return false;
686 }
687 return true;
688}
689
690
691bool
692find_and_go_to_workspace(system_path const & search_root)
693{
694 fs::path root(search_root.as_external(), fs::native);
695 fs::path bookdir(bookkeeping_root.as_external(), fs::native);
696 fs::path oldbookdir(old_bookkeeping_root.as_external(), fs::native);
697 fs::path current, removed;
698
699 // first look for the current name of the bookkeeping directory.
700 // if we don't find it, look for it under the old name, so that
701 // migration has a chance to work.
702 if (!find_bookdir(root, bookdir, current, removed))
703 if (!find_bookdir(root, oldbookdir, current, removed))
704 return false;
705
706 working_root.set(current.native_file_string(), true);
707 initial_rel_path.set(removed, true);
708
709 L(FL("working root is '%s'") % working_root.get_but_unused());
710 L(FL("initial relative path is '%s'") % initial_rel_path.get_but_unused().string());
711
712 change_current_working_dir(working_root.get_but_unused());
713
714 return true;
715}
716
717void
718go_to_workspace(system_path const & new_workspace)
719{
720 working_root.set(new_workspace, true);
721 initial_rel_path.set(fs::path(), true);
722 change_current_working_dir(new_workspace);
723}
724
725void
726mark_std_paths_used(void)
727{
728 working_root.get();
729 initial_rel_path.get();
730}
731
732///////////////////////////////////////////////////////////////////////////
733// tests
734///////////////////////////////////////////////////////////////////////////
735
736#ifdef BUILD_UNIT_TESTS
737#include "unit_tests.hh"
738
739using std::logic_error;
740
741UNIT_TEST(paths, null_name)
742{
743 BOOST_CHECK(null_name(the_null_component));
744}
745
746UNIT_TEST(paths, file_path_internal)
747{
748 char const * baddies[] = {"/foo",
749 "foo//bar",
750 "foo/../bar",
751 "../bar",
752 "_MTN",
753 "_MTN/blah",
754 "foo/bar/",
755 "foo/bar/.",
756 "foo/bar/./",
757 "foo/./bar",
758 "./foo",
759 ".",
760 "..",
761 "c:\\foo",
762 "c:foo",
763 "c:/foo",
764 // some baddies made bad by a security kluge --
765 // see the comment in in_bookkeeping_dir
766 "_mtn",
767 "_mtN",
768 "_mTn",
769 "_Mtn",
770 "_MTn",
771 "_MtN",
772 "_mTN",
773 "_mtn/foo",
774 "_mtN/foo",
775 "_mTn/foo",
776 "_Mtn/foo",
777 "_MTn/foo",
778 "_MtN/foo",
779 "_mTN/foo",
780 0 };
781 initial_rel_path.unset();
782 initial_rel_path.set(fs::path(), true);
783 for (char const ** c = baddies; *c; ++c)
784 {
785 BOOST_CHECK_THROW(file_path_internal(*c), logic_error);
786 }
787 initial_rel_path.unset();
788 initial_rel_path.set(fs::path("blah/blah/blah", fs::native), true);
789 for (char const ** c = baddies; *c; ++c)
790 {
791 BOOST_CHECK_THROW(file_path_internal(*c), logic_error);
792 }
793
794 BOOST_CHECK(file_path().empty());
795 BOOST_CHECK(file_path_internal("").empty());
796
797 char const * goodies[] = {"",
798 "a",
799 "foo",
800 "foo/bar/baz",
801 "foo/bar.baz",
802 "foo/with-hyphen/bar",
803 "foo/with_underscore/bar",
804 "foo/with,other+@weird*%#$=stuff/bar",
805 ".foo/bar",
806 "..foo/bar",
807 "_MTNfoo/bar",
808 "foo:bar",
809 0 };
810
811 for (int i = 0; i < 2; ++i)
812 {
813 initial_rel_path.unset();
814 initial_rel_path.set(i ? fs::path()
815 : fs::path("blah/blah/blah", fs::native),
816 true);
817 for (char const ** c = goodies; *c; ++c)
818 {
819 file_path fp = file_path_internal(*c);
820 BOOST_CHECK(fp.as_internal() == *c);
821 BOOST_CHECK(file_path_internal(fp.as_internal()) == fp);
822 split_path split_test;
823 fp.split(split_test);
824 BOOST_CHECK(!split_test.empty());
825 file_path fp2(split_test);
826 BOOST_CHECK(fp == fp2);
827 BOOST_CHECK(null_name(split_test[0]));
828 for (split_path::const_iterator
829 i = split_test.begin() + 1; i != split_test.end(); ++i)
830 BOOST_CHECK(!null_name(*i));
831 }
832 }
833
834 initial_rel_path.unset();
835}
836
837static void check_fp_normalizes_to(char * before, char * after)
838{
839 L(FL("check_fp_normalizes_to: '%s' -> '%s'") % before % after);
840 file_path fp = file_path_external(utf8(before));
841 L(FL(" (got: %s)") % fp);
842 BOOST_CHECK(fp.as_internal() == after);
843 BOOST_CHECK(file_path_internal(fp.as_internal()) == fp);
844 // we compare after to the external form too, since as far as we know
845 // relative normalized posix paths are always good win32 paths too
846 BOOST_CHECK(fp.as_external() == after);
847 split_path split_test;
848 fp.split(split_test);
849 BOOST_CHECK(!split_test.empty());
850 file_path fp2(split_test);
851 BOOST_CHECK(fp == fp2);
852 BOOST_CHECK(null_name(split_test[0]));
853 for (split_path::const_iterator
854 i = split_test.begin() + 1; i != split_test.end(); ++i)
855 BOOST_CHECK(!null_name(*i));
856}
857
858UNIT_TEST(paths, file_path_external_null_prefix)
859{
860 initial_rel_path.unset();
861 initial_rel_path.set(fs::path(), true);
862
863 char const * baddies[] = {"/foo",
864 "../bar",
865 "_MTN/blah",
866 "_MTN",
867 "//blah",
868 "\\foo",
869 "..",
870 "c:\\foo",
871 "c:foo",
872 "c:/foo",
873 "",
874 // some baddies made bad by a security kluge --
875 // see the comment in in_bookkeeping_dir
876 "_mtn",
877 "_mtN",
878 "_mTn",
879 "_Mtn",
880 "_MTn",
881 "_MtN",
882 "_mTN",
883 "_mtn/foo",
884 "_mtN/foo",
885 "_mTn/foo",
886 "_Mtn/foo",
887 "_MTn/foo",
888 "_MtN/foo",
889 "_mTN/foo",
890 0 };
891 for (char const ** c = baddies; *c; ++c)
892 {
893 L(FL("test_file_path_external_null_prefix: trying baddie: %s") % *c);
894 BOOST_CHECK_THROW(file_path_external(utf8(*c)), informative_failure);
895 }
896
897 check_fp_normalizes_to("a", "a");
898 check_fp_normalizes_to("foo", "foo");
899 check_fp_normalizes_to("foo/bar", "foo/bar");
900 check_fp_normalizes_to("foo/bar/baz", "foo/bar/baz");
901 check_fp_normalizes_to("foo/bar.baz", "foo/bar.baz");
902 check_fp_normalizes_to("foo/with-hyphen/bar", "foo/with-hyphen/bar");
903 check_fp_normalizes_to("foo/with_underscore/bar", "foo/with_underscore/bar");
904 check_fp_normalizes_to(".foo/bar", ".foo/bar");
905 check_fp_normalizes_to("..foo/bar", "..foo/bar");
906 check_fp_normalizes_to(".", "");
907#ifndef WIN32
908 check_fp_normalizes_to("foo:bar", "foo:bar");
909#endif
910 check_fp_normalizes_to("foo/with,other+@weird*%#$=stuff/bar",
911 "foo/with,other+@weird*%#$=stuff/bar");
912
913 // Why are these tests with // in them commented out? because boost::fs
914 // sucks and can't normalize them. FIXME.
915 //check_fp_normalizes_to("foo//bar", "foo/bar");
916 check_fp_normalizes_to("foo/../bar", "bar");
917 check_fp_normalizes_to("foo/bar/", "foo/bar");
918 check_fp_normalizes_to("foo/bar/.", "foo/bar");
919 check_fp_normalizes_to("foo/bar/./", "foo/bar");
920 check_fp_normalizes_to("foo/./bar/", "foo/bar");
921 check_fp_normalizes_to("./foo", "foo");
922 //check_fp_normalizes_to("foo///.//", "foo");
923
924 initial_rel_path.unset();
925}
926
927UNIT_TEST(paths, file_path_external_prefix__MTN)
928{
929 initial_rel_path.unset();
930 initial_rel_path.set(fs::path("_MTN"), true);
931
932 BOOST_CHECK_THROW(file_path_external(utf8("foo")), informative_failure);
933 BOOST_CHECK_THROW(file_path_external(utf8(".")), informative_failure);
934 BOOST_CHECK_THROW(file_path_external(utf8("./blah")), informative_failure);
935 check_fp_normalizes_to("..", "");
936 check_fp_normalizes_to("../foo", "foo");
937}
938
939UNIT_TEST(paths, file_path_external_prefix_a_b)
940{
941 initial_rel_path.unset();
942 initial_rel_path.set(fs::path("a/b"), true);
943
944 char const * baddies[] = {"/foo",
945 "../../../bar",
946 "../../..",
947 "../../_MTN",
948 "../../_MTN/foo",
949 "//blah",
950 "\\foo",
951 "c:\\foo",
952#ifdef WIN32
953 "c:foo",
954 "c:/foo",
955#endif
956 "",
957 // some baddies made bad by a security kluge --
958 // see the comment in in_bookkeeping_dir
959 "../../_mtn",
960 "../../_mtN",
961 "../../_mTn",
962 "../../_Mtn",
963 "../../_MTn",
964 "../../_MtN",
965 "../../_mTN",
966 "../../_mtn/foo",
967 "../../_mtN/foo",
968 "../../_mTn/foo",
969 "../../_Mtn/foo",
970 "../../_MTn/foo",
971 "../../_MtN/foo",
972 "../../_mTN/foo",
973 0 };
974 for (char const ** c = baddies; *c; ++c)
975 {
976 L(FL("test_file_path_external_prefix_a_b: trying baddie: %s") % *c);
977 BOOST_CHECK_THROW(file_path_external(utf8(*c)), informative_failure);
978 }
979
980 check_fp_normalizes_to("foo", "a/b/foo");
981 check_fp_normalizes_to("a", "a/b/a");
982 check_fp_normalizes_to("foo/bar", "a/b/foo/bar");
983 check_fp_normalizes_to("foo/bar/baz", "a/b/foo/bar/baz");
984 check_fp_normalizes_to("foo/bar.baz", "a/b/foo/bar.baz");
985 check_fp_normalizes_to("foo/with-hyphen/bar", "a/b/foo/with-hyphen/bar");
986 check_fp_normalizes_to("foo/with_underscore/bar", "a/b/foo/with_underscore/bar");
987 check_fp_normalizes_to(".foo/bar", "a/b/.foo/bar");
988 check_fp_normalizes_to("..foo/bar", "a/b/..foo/bar");
989 check_fp_normalizes_to(".", "a/b");
990#ifndef WIN32
991 check_fp_normalizes_to("foo:bar", "a/b/foo:bar");
992#endif
993 check_fp_normalizes_to("foo/with,other+@weird*%#$=stuff/bar",
994 "a/b/foo/with,other+@weird*%#$=stuff/bar");
995 // why are the tests with // in them commented out? because boost::fs sucks
996 // and can't normalize them. FIXME.
997 //check_fp_normalizes_to("foo//bar", "a/b/foo/bar");
998 check_fp_normalizes_to("foo/../bar", "a/b/bar");
999 check_fp_normalizes_to("foo/bar/", "a/b/foo/bar");
1000 check_fp_normalizes_to("foo/bar/.", "a/b/foo/bar");
1001 check_fp_normalizes_to("foo/bar/./", "a/b/foo/bar");
1002 check_fp_normalizes_to("foo/./bar/", "a/b/foo/bar");
1003 check_fp_normalizes_to("./foo", "a/b/foo");
1004 //check_fp_normalizes_to("foo///.//", "a/b/foo");
1005 // things that would have been bad without the initial_rel_path:
1006 check_fp_normalizes_to("../foo", "a/foo");
1007 check_fp_normalizes_to("..", "a");
1008 check_fp_normalizes_to("../..", "");
1009 check_fp_normalizes_to("_MTN/foo", "a/b/_MTN/foo");
1010 check_fp_normalizes_to("_MTN", "a/b/_MTN");
1011#ifndef WIN32
1012 check_fp_normalizes_to("c:foo", "a/b/c:foo");
1013 check_fp_normalizes_to("c:/foo", "a/b/c:/foo");
1014#endif
1015
1016 initial_rel_path.unset();
1017}
1018
1019UNIT_TEST(paths, split_join)
1020{
1021 file_path fp1 = file_path_internal("foo/bar/baz");
1022 file_path fp2 = file_path_internal("bar/baz/foo");
1023 split_path split1, split2;
1024 fp1.split(split1);
1025 fp2.split(split2);
1026 BOOST_CHECK(fp1 == file_path(split1));
1027 BOOST_CHECK(fp2 == file_path(split2));
1028 BOOST_CHECK(!(fp1 == file_path(split2)));
1029 BOOST_CHECK(!(fp2 == file_path(split1)));
1030 BOOST_CHECK(split1.size() == 4);
1031 BOOST_CHECK(split2.size() == 4);
1032 BOOST_CHECK(split1[1] != split1[2]);
1033 BOOST_CHECK(split1[1] != split1[3]);
1034 BOOST_CHECK(split1[2] != split1[3]);
1035 BOOST_CHECK(null_name(split1[0])
1036 && !null_name(split1[1])
1037 && !null_name(split1[2])
1038 && !null_name(split1[3]));
1039 BOOST_CHECK(split1[1] == split2[3]);
1040 BOOST_CHECK(split1[2] == split2[1]);
1041 BOOST_CHECK(split1[3] == split2[2]);
1042
1043 file_path fp3 = file_path_internal("");
1044 split_path split3;
1045 fp3.split(split3);
1046 BOOST_CHECK(split3.size() == 1 && null_name(split3[0]));
1047
1048 // empty split_path is invalid
1049 split_path split4;
1050 // this comparison tricks the compiler into not completely eliminating this
1051 // code as dead...
1052 BOOST_CHECK_THROW(file_path(split4) == file_path(), logic_error);
1053 split4.push_back(the_null_component);
1054 BOOST_CHECK(file_path(split4) == file_path());
1055
1056 // split_path without null first item is invalid
1057 split4.clear();
1058 split4.push_back(split1[1]);
1059 // this comparison tricks the compiler into not completely eliminating this
1060 // code as dead...
1061 BOOST_CHECK_THROW(file_path(split4) == file_path(), logic_error);
1062
1063 // split_path with non-first item item null is invalid
1064 split4.clear();
1065 split4.push_back(the_null_component);
1066 split4.push_back(split1[0]);
1067 split4.push_back(the_null_component);
1068 // this comparison tricks the compiler into not completely eliminating this
1069 // code as dead...
1070 BOOST_CHECK_THROW(file_path(split4) == file_path(), logic_error);
1071
1072 // Make sure that we can't use joining to create a path into the bookkeeping
1073 // dir
1074 {
1075 split_path split_mt1, split_mt2;
1076 file_path_internal("foo/_MTN").split(split_mt1);
1077 BOOST_CHECK(split_mt1.size() == 3);
1078 I(split_mt1[2] == bookkeeping_root_component);
1079 split_mt2.push_back(the_null_component);
1080 split_mt2.push_back(split_mt1[2]);
1081 // split_mt2 now contains the component "_MTN"
1082 BOOST_CHECK_THROW(file_path(split_mt2) == file_path(), logic_error);
1083 split_mt2.push_back(split_mt1[1]);
1084 // split_mt2 now contains the components "_MTN", "foo" in that order
1085 // this comparison tricks the compiler into not completely eliminating this
1086 // code as dead...
1087 BOOST_CHECK_THROW(file_path(split_mt2) == file_path(), logic_error);
1088 }
1089 // and make sure it fails for the klugy security cases -- see comments on
1090 // in_bookkeeping_dir
1091 {
1092 split_path split_mt1, split_mt2;
1093 file_path_internal("foo/_mTn").split(split_mt1);
1094 BOOST_CHECK(split_mt1.size() == 3);
1095 split_mt2.push_back(the_null_component);
1096 split_mt2.push_back(split_mt1[2]);
1097 // split_mt2 now contains the component "_mTn"
1098 BOOST_CHECK_THROW(file_path(split_mt2) == file_path(), logic_error);
1099 split_mt2.push_back(split_mt1[1]);
1100 // split_mt2 now contains the components "_mTn", "foo" in that order
1101 // this comparison tricks the compiler into not completely eliminating this
1102 // code as dead...
1103 BOOST_CHECK_THROW(file_path(split_mt2) == file_path(), logic_error);
1104 }
1105}
1106
1107static void check_bk_normalizes_to(char * before, char * after)
1108{
1109 bookkeeping_path bp(bookkeeping_root / before);
1110 L(FL("normalizing %s to %s (got %s)") % before % after % bp);
1111 BOOST_CHECK(bp.as_external() == after);
1112 BOOST_CHECK(bookkeeping_path(bp.as_internal()).as_internal() == bp.as_internal());
1113}
1114
1115UNIT_TEST(paths, bookkeeping)
1116{
1117 char const * baddies[] = {"/foo",
1118 "foo//bar",
1119 "foo/../bar",
1120 "../bar",
1121 "foo/bar/",
1122 "foo/bar/.",
1123 "foo/bar/./",
1124 "foo/./bar",
1125 "./foo",
1126 ".",
1127 "..",
1128 "c:\\foo",
1129 "c:foo",
1130 "c:/foo",
1131 "",
1132 "a:b",
1133 0 };
1134 string tmp_path_string;
1135
1136 for (char const ** c = baddies; *c; ++c)
1137 {
1138 L(FL("test_bookkeeping_path baddie: trying '%s'") % *c);
1139 BOOST_CHECK_THROW(bookkeeping_path(tmp_path_string.assign(*c)), logic_error);
1140 BOOST_CHECK_THROW(bookkeeping_root / tmp_path_string.assign(*c), logic_error);
1141 }
1142 BOOST_CHECK_THROW(bookkeeping_path(tmp_path_string.assign("foo/bar")), logic_error);
1143 BOOST_CHECK_THROW(bookkeeping_path(tmp_path_string.assign("a")), logic_error);
1144
1145 check_bk_normalizes_to("a", "_MTN/a");
1146 check_bk_normalizes_to("foo", "_MTN/foo");
1147 check_bk_normalizes_to("foo/bar", "_MTN/foo/bar");
1148 check_bk_normalizes_to("foo/bar/baz", "_MTN/foo/bar/baz");
1149}
1150
1151static void check_system_normalizes_to(char * before, char * after)
1152{
1153 system_path sp(before);
1154 L(FL("normalizing '%s' to '%s' (got '%s')") % before % after % sp);
1155 BOOST_CHECK(sp.as_external() == after);
1156 BOOST_CHECK(system_path(sp.as_internal()).as_internal() == sp.as_internal());
1157}
1158
1159UNIT_TEST(paths, system)
1160{
1161 initial_abs_path.unset();
1162 initial_abs_path.set(system_path("/a/b"), true);
1163
1164 BOOST_CHECK_THROW(system_path(""), informative_failure);
1165
1166 check_system_normalizes_to("foo", "/a/b/foo");
1167 check_system_normalizes_to("foo/bar", "/a/b/foo/bar");
1168 check_system_normalizes_to("/foo/bar", "/foo/bar");
1169 check_system_normalizes_to("//foo/bar", "//foo/bar");
1170#ifdef WIN32
1171 check_system_normalizes_to("c:foo", "c:foo");
1172 check_system_normalizes_to("c:/foo", "c:/foo");
1173 check_system_normalizes_to("c:\\foo", "c:/foo");
1174#else
1175 check_system_normalizes_to("c:foo", "/a/b/c:foo");
1176 check_system_normalizes_to("c:/foo", "/a/b/c:/foo");
1177 check_system_normalizes_to("c:\\foo", "/a/b/c:\\foo");
1178 check_system_normalizes_to("foo:bar", "/a/b/foo:bar");
1179#endif
1180 // we require that system_path normalize out ..'s, because of the following
1181 // case:
1182 // /work mkdir newdir
1183 // /work$ cd newdir
1184 // /work/newdir$ monotone setup --db=../foo.db
1185 // Now they have either "/work/foo.db" or "/work/newdir/../foo.db" in
1186 // _MTN/options
1187 // /work/newdir$ cd ..
1188 // /work$ mv newdir newerdir # better name
1189 // Oops, now, if we stored the version with ..'s in, this workspace
1190 // is broken.
1191 check_system_normalizes_to("../foo", "/a/foo");
1192 check_system_normalizes_to("foo/..", "/a/b");
1193 check_system_normalizes_to("/foo/bar/..", "/foo");
1194 check_system_normalizes_to("/foo/..", "/");
1195 // can't do particularly interesting checking of tilde expansion, but at
1196 // least we can check that it's doing _something_...
1197 string tilde_expanded = system_path("~/foo").as_external();
1198#ifdef WIN32
1199 BOOST_CHECK(tilde_expanded[1] == ':');
1200#else
1201 BOOST_CHECK(tilde_expanded[0] == '/');
1202#endif
1203 BOOST_CHECK(tilde_expanded.find('~') == string::npos);
1204 // and check for the weird WIN32 version
1205#ifdef WIN32
1206 string tilde_expanded2 = system_path("~this_user_does_not_exist_anywhere").as_external();
1207 BOOST_CHECK(tilde_expanded2[1] == ':');
1208 BOOST_CHECK(tilde_expanded2.find('~') == string::npos);
1209#else
1210 BOOST_CHECK_THROW(system_path("~this_user_does_not_exist_anywhere"), informative_failure);
1211#endif
1212
1213 // finally, make sure that the copy-from-any_path constructor works right
1214 // in particular, it should interpret the paths it gets as being relative to
1215 // the project root, not the initial path
1216 working_root.unset();
1217 working_root.set(system_path("/working/root"), true);
1218 initial_rel_path.unset();
1219 initial_rel_path.set(fs::path("rel/initial"), true);
1220
1221 BOOST_CHECK(system_path(system_path("foo/bar")).as_internal() == "/a/b/foo/bar");
1222 BOOST_CHECK(!working_root.used);
1223 BOOST_CHECK(system_path(system_path("/foo/bar")).as_internal() == "/foo/bar");
1224 BOOST_CHECK(!working_root.used);
1225 BOOST_CHECK(system_path(file_path_internal("foo/bar"), false).as_internal()
1226 == "/working/root/foo/bar");
1227 BOOST_CHECK(!working_root.used);
1228 BOOST_CHECK(system_path(file_path_internal("foo/bar")).as_internal()
1229 == "/working/root/foo/bar");
1230 BOOST_CHECK(working_root.used);
1231 BOOST_CHECK(system_path(file_path_external(utf8("foo/bar"))).as_external()
1232 == "/working/root/rel/initial/foo/bar");
1233 file_path a_file_path;
1234 BOOST_CHECK(system_path(a_file_path).as_external()
1235 == "/working/root");
1236 BOOST_CHECK(system_path(bookkeeping_path("_MTN/foo/bar")).as_internal()
1237 == "/working/root/_MTN/foo/bar");
1238 BOOST_CHECK(system_path(bookkeeping_root).as_internal()
1239 == "/working/root/_MTN");
1240 initial_abs_path.unset();
1241 working_root.unset();
1242 initial_rel_path.unset();
1243}
1244
1245UNIT_TEST(paths, access_tracker)
1246{
1247 access_tracker<int> a;
1248 BOOST_CHECK_THROW(a.get(), logic_error);
1249 a.set(1, false);
1250 BOOST_CHECK_THROW(a.set(2, false), logic_error);
1251 a.set(2, true);
1252 BOOST_CHECK_THROW(a.set(3, false), logic_error);
1253 BOOST_CHECK(a.get() == 2);
1254 BOOST_CHECK_THROW(a.set(3, true), logic_error);
1255 a.unset();
1256 a.may_not_initialize();
1257 BOOST_CHECK_THROW(a.set(1, false), logic_error);
1258 BOOST_CHECK_THROW(a.set(2, true), logic_error);
1259 a.unset();
1260 a.set(1, false);
1261 BOOST_CHECK_THROW(a.may_not_initialize(), logic_error);
1262}
1263
1264static void test_a_path_ordering(string const & left, string const & right)
1265{
1266 MM(left);
1267 MM(right);
1268 split_path left_sp, right_sp;
1269 file_path_internal(left).split(left_sp);
1270 file_path_internal(right).split(right_sp);
1271 I(left_sp < right_sp);
1272}
1273
1274UNIT_TEST(paths, ordering)
1275{
1276 // this ordering is very important:
1277 // -- it is used to determine the textual form of csets and manifests
1278 // (in particular, it cannot be changed)
1279 // -- it is used to determine in what order cset operations can be applied
1280 // (in particular, foo must sort before foo/bar, so that we can use it
1281 // to do top-down and bottom-up traversals of a set of paths).
1282 test_a_path_ordering("a", "b");
1283 test_a_path_ordering("a", "c");
1284 test_a_path_ordering("ab", "ac");
1285 test_a_path_ordering("a", "ab");
1286 test_a_path_ordering("", "a");
1287 test_a_path_ordering("", ".foo");
1288 test_a_path_ordering("foo", "foo/bar");
1289 // . is before / asciibetically, so sorting by strings will give the wrong
1290 // answer on this:
1291 test_a_path_ordering("foo/bar", "foo.bar");
1292
1293 // path_components used to be interned strings, and we used the default sort
1294 // order, which meant that in practice path components would sort in the
1295 // _order they were first used in the program_. So let's put in a test that
1296 // would catch this sort of brokenness.
1297 test_a_path_ordering("fallanopic_not_otherwise_mentioned", "xyzzy");
1298 test_a_path_ordering("fallanoooo_not_otherwise_mentioned_and_smaller", "fallanopic_not_otherwise_mentioned");
1299}
1300
1301UNIT_TEST(paths, test_internal_string_is_bookkeeping_path)
1302{
1303 char const * yes[] = {"_MTN",
1304 "_MTN/foo",
1305 "_mtn/Foo",
1306 0 };
1307 char const * no[] = {"foo/_MTN",
1308 "foo/bar",
1309 0 };
1310 for (char const ** c = yes; *c; ++c)
1311 BOOST_CHECK(bookkeeping_path
1312 ::internal_string_is_bookkeeping_path(utf8(std::string(*c))));
1313 for (char const ** c = no; *c; ++c)
1314 BOOST_CHECK(!bookkeeping_path
1315 ::internal_string_is_bookkeeping_path(utf8(std::string(*c))));
1316}
1317
1318UNIT_TEST(paths, test_external_string_is_bookkeeping_path_prefix_none)
1319{
1320 initial_rel_path.unset();
1321 initial_rel_path.set(fs::path(), true);
1322
1323 char const * yes[] = {"_MTN",
1324 "_MTN/foo",
1325 "_mtn/Foo",
1326 "_MTN/foo/..",
1327 0 };
1328 char const * no[] = {"foo/_MTN",
1329 "foo/bar",
1330 "_MTN/..",
1331 0 };
1332 for (char const ** c = yes; *c; ++c)
1333 BOOST_CHECK(bookkeeping_path
1334 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1335 for (char const ** c = no; *c; ++c)
1336 BOOST_CHECK(!bookkeeping_path
1337 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1338}
1339
1340UNIT_TEST(paths, test_external_string_is_bookkeeping_path_prefix_a_b)
1341{
1342 initial_rel_path.unset();
1343 initial_rel_path.set(fs::path("a/b"), true);
1344
1345 char const * yes[] = {"../../_MTN",
1346 "../../_MTN/foo",
1347 "../../_mtn/Foo",
1348 "../../_MTN/foo/..",
1349 "../../foo/../_MTN/foo",
1350 0 };
1351 char const * no[] = {"foo/_MTN",
1352 "foo/bar",
1353 "_MTN",
1354 "../../foo/_MTN",
1355 0 };
1356 for (char const ** c = yes; *c; ++c)
1357 BOOST_CHECK(bookkeeping_path
1358 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1359 for (char const ** c = no; *c; ++c)
1360 BOOST_CHECK(!bookkeeping_path
1361 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1362}
1363
1364UNIT_TEST(paths, test_external_string_is_bookkeeping_path_prefix__MTN)
1365{
1366 initial_rel_path.unset();
1367 initial_rel_path.set(fs::path("_MTN"), true);
1368
1369 char const * yes[] = {".",
1370 "foo",
1371 "../_MTN/foo/..",
1372 "../_mtn/foo",
1373 "../foo/../_MTN/foo",
1374 0 };
1375 char const * no[] = {"../foo",
1376 "../foo/bar",
1377 "../foo/_MTN",
1378 0 };
1379 for (char const ** c = yes; *c; ++c)
1380 BOOST_CHECK(bookkeeping_path
1381 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1382 for (char const ** c = no; *c; ++c)
1383 BOOST_CHECK(!bookkeeping_path
1384 ::external_string_is_bookkeeping_path(utf8(std::string(*c))));
1385}
1386
1387#endif // BUILD_UNIT_TESTS
1388
1389// Local Variables:
1390// mode: C++
1391// fill-column: 76
1392// c-file-style: "gnu"
1393// indent-tabs-mode: nil
1394// End:
1395// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status