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