1/*
2 * Copyright 2016 Software Freedom Conservancy Inc.
3 * Copyright 2018-2019 Michael Gratton <mike@vee.net>.
4 *
5 * This software is licensed under the GNU Lesser General Public License
6 * (version 2.1 or later).  See the COPYING file in this distribution.
7 */
8
9/**
10 * A generic structure for representing and maintaining folder paths.
11 *
12 * A FolderPath may have one parent and one child.  A FolderPath without a parent is called a
13 * root folder can be be created with {@link FolderRoot}, which is a FolderPath.
14 *
15 * @see FolderRoot
16 */
17public class Geary.FolderPath :
18    BaseObject, Gee.Hashable<FolderPath>, Gee.Comparable<FolderPath> {
19
20
21    /** Type of the GLib.Variant used to represent folder paths */
22    public const string VARIANT_TYPE = "(sas)";
23
24
25    // Workaround for Vala issue #659. See children below.
26    private class FolderPathWeakRef {
27
28        GLib.WeakRef weak_ref;
29
30        public FolderPathWeakRef(FolderPath path) {
31            this.weak_ref = GLib.WeakRef(path);
32        }
33
34        public FolderPath? get() {
35            return this.weak_ref.get() as FolderPath;
36        }
37
38    }
39
40
41    /** The base name of this folder, excluding parents. */
42    public string name { get; private set; }
43
44    /** The number of children under the root in this path. */
45    public uint length {
46        get {
47            uint length = 0;
48            FolderPath parent = this.parent;
49            while (parent != null) {
50                length++;
51                parent = parent.parent;
52            }
53            return length;
54        }
55    }
56
57    /**
58     * Whether this path is lexiographically case-sensitive.
59     *
60     * This has implications, as {@link FolderPath} is Comparable and Hashable.
61     */
62    public bool case_sensitive { get; private set; }
63
64    /** Determines if this path is a root folder path. */
65    public bool is_root {
66        get { return this.parent == null; }
67    }
68
69    /** Determines if this path is a child of the root folder. */
70    public bool is_top_level {
71        get {
72            FolderPath? parent = parent;
73            return parent != null && parent.is_root;
74        }
75    }
76
77    /** Returns the parent of this path. */
78    public FolderPath? parent { get; private set; }
79
80    private string[] path;
81
82    // Would use a `weak FolderPath` value type for this map instead of
83    // the custom class, but we can't currently reassign built-in
84    // weak refs back to a strong ref at the moment, nor use a
85    // GLib.WeakRef as a generics param. See Vala issue #659.
86    private Gee.Map<string,FolderPathWeakRef?> children =
87        new Gee.HashMap<string,FolderPathWeakRef?>();
88
89    private uint? stored_hash = null;
90
91
92    /** Constructor only for use by {@link FolderRoot}. */
93    internal FolderPath() {
94        this.name = "";
95        this.parent = null;
96        this.case_sensitive = false;
97        this.path = new string[0];
98    }
99
100    private FolderPath.child(FolderPath parent,
101                             string name,
102                             bool case_sensitive) {
103        this.parent = parent;
104        this.name = name;
105        this.case_sensitive = case_sensitive;
106        this.path = parent.path.copy();
107        this.path += name;
108    }
109
110    /**
111     * Returns the {@link FolderRoot} of this path.
112     */
113    public Geary.FolderRoot get_root() {
114        FolderPath? path = this;
115        while (path.parent != null) {
116            path = path.parent;
117        }
118        return (FolderRoot) path;
119    }
120
121    /**
122     * Returns an array of the names of non-root elements in the path.
123     */
124    public string[] as_array() {
125        return this.path;
126    }
127
128    /**
129     * Creates a path that is a child of this folder.
130     *
131     * Specifying {@link Trillian.TRUE} or {@link Trillian.FALSE} for
132     * `is_case_sensitive` forces case-sensitivity either way. If
133     * {@link Trillian.UNKNOWN}, then {@link
134     * FolderRoot.default_case_sensitivity} is used.
135     */
136    public virtual FolderPath
137        get_child(string name,
138                  Trillian is_case_sensitive = Trillian.UNKNOWN) {
139        FolderPath? child = null;
140        FolderPathWeakRef? child_ref = this.children.get(name);
141        if (child_ref != null) {
142            child = child_ref.get();
143        }
144        if (child == null) {
145            child = new FolderPath.child(
146                this,
147                name,
148                is_case_sensitive.to_boolean(
149                    get_root().default_case_sensitivity
150                )
151            );
152            this.children.set(name, new FolderPathWeakRef(child));
153        }
154        return child;
155    }
156
157    /**
158     * Determines if this path is a strict ancestor of another.
159     */
160    public bool is_descendant(FolderPath target) {
161        bool is_descendent = false;
162        FolderPath? path = target.parent;
163        while (path != null) {
164            if (path.equal_to(this)) {
165                is_descendent = true;
166                break;
167            }
168            path = path.parent;
169        }
170        return is_descendent;
171    }
172
173    /**
174     * Does a Unicode-normalized, case insensitive match.  Useful for
175     * getting a rough idea if a folder matches a name, but shouldn't
176     * be used to determine strict equality.
177     */
178    public int compare_normalized_ci(FolderPath other) {
179        return compare_internal(other, false, true);
180    }
181
182    /**
183     * {@inheritDoc}
184     *
185     * Comparisons for FolderPath is defined as (a) empty paths
186     * are less-than non-empty paths and (b) each element is compared
187     * to the corresponding path element of the other FolderPath
188     * following collation rules for casefolded (case-insensitive)
189     * compared, and (c) shorter paths are less-than longer paths,
190     * assuming the path elements are equal up to the shorter path's
191     * length.
192     *
193     * Note that {@link FolderPath.case_sensitive} affects comparisons.
194     *
195     * Returns -1 if this path is lexiographically before the other, 1
196     * if its after, and 0 if they are equal.
197     */
198    public int compare_to(FolderPath other) {
199        return compare_internal(other, true, false);
200    }
201
202    /**
203     * {@inheritDoc}
204     *
205     * Note that {@link FolderPath.case_sensitive} affects comparisons.
206     */
207    public uint hash() {
208        if (this.stored_hash == null) {
209            this.stored_hash = 0;
210            FolderPath? path = this;
211            while (path != null) {
212                this.stored_hash ^= (case_sensitive)
213                    ? str_hash(path.name) : str_hash(path.name.down());
214                path = path.parent;
215            }
216        }
217        return this.stored_hash;
218    }
219
220    /** {@inheritDoc} */
221    public bool equal_to(FolderPath other) {
222        return this.compare_internal(other, true, false) == 0;
223    }
224
225    /**
226     * Returns a representation useful for serialisation.
227     *
228     * This can be used to transmit folder paths as D-Bus method and
229     * GLib Action parameters, and so on.
230     *
231     * @return a serialised form of this path, that will match the
232     * GVariantType specified by {@link VARIANT_TYPE}.
233     * @see FolderRoot.from_variant
234     */
235    public GLib.Variant to_variant() {
236        return new GLib.Variant.tuple(new GLib.Variant[] {
237                get_root().label,
238                as_array()
239        });
240    }
241
242    /**
243     * Returns a representation useful for debugging.
244     *
245     * Do not use this for obtaining an IMAP mailbox name to send to a
246     * server, use {@link
247     * Geary.Imap.MailboxSpecifier.MailboxSpecifier.from_folder_path}
248     * instead. This method is useful for debugging and logging only.
249     */
250    public string to_string() {
251        const char SEP = '>';
252        StringBuilder builder = new StringBuilder();
253        if (this.is_root) {
254            builder.append_c(SEP);
255        } else {
256            foreach (string name in this.path) {
257                builder.append_c(SEP);
258                builder.append(name);
259            }
260        }
261        return builder.str;
262    }
263
264    private int compare_internal(FolderPath other,
265                                 bool allow_case_sensitive,
266                                 bool normalize) {
267        if (this == other) {
268            return 0;
269        }
270
271        int a_len = (int) this.length;
272        int b_len = (int) other.length;
273        if (a_len != b_len) {
274            return a_len - b_len;
275        }
276
277        return compare_names(this, other, allow_case_sensitive, normalize);
278    }
279
280    private static int compare_names(FolderPath a, FolderPath b,
281                                     bool allow_case_sensitive,
282                                     bool normalize) {
283        int cmp = 0;
284        if (a.parent == null && b.parent == null) {
285            cmp = strcmp(((FolderRoot) a).label, ((FolderRoot) b).label);
286        } else {
287            cmp = compare_names(
288                a.parent, b.parent, allow_case_sensitive, normalize
289            );
290        }
291
292        if (cmp == 0) {
293            string a_name = a.name;
294            string b_name = b.name;
295
296            if (normalize) {
297                a_name = a_name.normalize();
298                b_name = b_name.normalize();
299            }
300
301            if (!allow_case_sensitive
302                // if either case-sensitive, then comparison is CS
303                || (!a.case_sensitive && !b.case_sensitive)) {
304                a_name = a_name.casefold();
305                b_name = b_name.casefold();
306            }
307
308            return strcmp(a_name, b_name);
309        }
310        return cmp;
311    }
312
313}
314
315
316/**
317 * The root of a folder hierarchy.
318 *
319 * A {@link FolderPath} can only be created by starting with a
320 * FolderRoot and adding children via {@link FolderPath.get_child}.
321 * Because all FolderPaths hold references to their parents, this
322 * element can be retrieved with {@link FolderPath.get_root}.
323 */
324public class Geary.FolderRoot : FolderPath {
325
326
327    /**
328     * A label for a folder root.
329     *
330     * Since there may be multiple folder roots (for example, local
331     * and remote folders, or for different remote namespaces), the
332     * label can be used to look up a specific root.
333     */
334    public string label { get; private set; }
335
336    /**
337     * The default case sensitivity of descendant folders.
338     *
339     * @see FolderPath.get_child
340     */
341    public bool default_case_sensitivity { get; private set; }
342
343
344    /**
345     * Constructs a new folder root with given default sensitivity.
346     */
347    public FolderRoot(string label, bool default_case_sensitivity) {
348        base();
349        this.label = label;
350        this.default_case_sensitivity = default_case_sensitivity;
351    }
352
353    /**
354     * Copies a folder path using this as the root.
355     *
356     * This method can be used to simply copy a path, or change the
357     * root that a path is attached to.
358     */
359    public FolderPath copy(FolderPath original) {
360        FolderPath copy = this;
361        foreach (string step in original.as_array()) {
362            copy = copy.get_child(step);
363        }
364        return copy;
365    }
366
367    /**
368     * Reconstructs a path under this root from a GLib variant.
369     *
370     * @see FolderPath.to_variant
371     * @throws EngineError.BAD_PARAMETERS when the variant is not the
372     * have the correct type or if the given root label does not match
373     * this root's label.
374     */
375    public FolderPath from_variant(GLib.Variant serialised)
376        throws EngineError.BAD_PARAMETERS {
377        if (serialised.get_type_string() != VARIANT_TYPE) {
378            throw new EngineError.BAD_PARAMETERS(
379                "Invalid serialised id type: %s", serialised.get_type_string()
380            );
381        }
382
383        string label = (string) serialised.get_child_value(0);
384        if (this.label != label) {
385            throw new EngineError.BAD_PARAMETERS(
386                "Invalid serialised folder root label: %s", label
387            );
388        }
389
390        FolderPath path = this;
391        foreach (string step in serialised.get_child_value(1).get_strv()) {
392            path = path.get_child(step);
393        }
394        return path;
395    }
396
397}
398