aboutsummaryrefslogtreecommitdiff
path: root/jimfs/src/main/java/com/google/common/jimfs/Directory.java
blob: aaab83b6096a6717742461bf76523bbead2cc86c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/*
 * Copyright 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.jimfs;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableSortedSet;
import java.util.Iterator;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

/**
 * A table of {@linkplain DirectoryEntry directory entries}.
 *
 * @author Colin Decker
 */
final class Directory extends File implements Iterable<DirectoryEntry> {

  /** The entry linking to this directory in its parent directory. */
  private DirectoryEntry entryInParent;

  /** Creates a new normal directory with the given ID. */
  public static Directory create(int id) {
    return new Directory(id);
  }

  /** Creates a new root directory with the given ID and name. */
  public static Directory createRoot(int id, Name name) {
    return new Directory(id, name);
  }

  private Directory(int id) {
    super(id);
    put(new DirectoryEntry(this, Name.SELF, this));
  }

  private Directory(int id, Name rootName) {
    this(id);
    linked(new DirectoryEntry(this, rootName, this));
  }

  /**
   * Creates a copy of this directory. The copy does <i>not</i> contain a copy of the entries in
   * this directory.
   */
  @Override
  Directory copyWithoutContent(int id) {
    return Directory.create(id);
  }

  /**
   * Returns the entry linking to this directory in its parent. If this directory has been deleted,
   * this returns the entry for it in the directory it was in when it was deleted.
   */
  public DirectoryEntry entryInParent() {
    return entryInParent;
  }

  /**
   * Returns the parent of this directory. If this directory has been deleted, this returns the
   * directory it was in when it was deleted.
   */
  public Directory parent() {
    return entryInParent.directory();
  }

  @Override
  void linked(DirectoryEntry entry) {
    File parent = entry.directory(); // handles null check
    this.entryInParent = entry;
    forcePut(new DirectoryEntry(this, Name.PARENT, parent));
  }

  @Override
  void unlinked() {
    // we don't actually remove the parent link when this directory is unlinked, but the parent's
    // link count should go down all the same
    parent().decrementLinkCount();
  }

  /** Returns the number of entries in this directory. */
  @VisibleForTesting
  int entryCount() {
    return entryCount;
  }

  /** Returns true if this directory has no entries other than those to itself and its parent. */
  public boolean isEmpty() {
    return entryCount() == 2;
  }

  /** Returns the entry for the given name in this table or null if no such entry exists. */
  @NullableDecl
  public DirectoryEntry get(Name name) {
    int index = bucketIndex(name, table.length);

    DirectoryEntry entry = table[index];
    while (entry != null) {
      if (name.equals(entry.name())) {
        return entry;
      }

      entry = entry.next;
    }
    return null;
  }

  /**
   * Links the given name to the given file in this directory.
   *
   * @throws IllegalArgumentException if {@code name} is a reserved name such as "." or if an entry
   *     already exists for the name
   */
  public void link(Name name, File file) {
    DirectoryEntry entry = new DirectoryEntry(this, checkNotReserved(name, "link"), file);
    put(entry);
    file.linked(entry);
  }

  /**
   * Unlinks the given name from the file it is linked to.
   *
   * @throws IllegalArgumentException if {@code name} is a reserved name such as "." or no entry
   *     exists for the name
   */
  public void unlink(Name name) {
    DirectoryEntry entry = remove(checkNotReserved(name, "unlink"));
    entry.file().unlinked();
  }

  /**
   * Creates an immutable sorted snapshot of the names this directory contains, excluding "." and
   * "..".
   */
  public ImmutableSortedSet<Name> snapshot() {
    ImmutableSortedSet.Builder<Name> builder =
        new ImmutableSortedSet.Builder<>(Name.displayOrdering());

    for (DirectoryEntry entry : this) {
      if (!isReserved(entry.name())) {
        builder.add(entry.name());
      }
    }

    return builder.build();
  }

  /** Checks that the given name is not "." or "..". Those names cannot be set/removed by users. */
  private static Name checkNotReserved(Name name, String action) {
    if (isReserved(name)) {
      throw new IllegalArgumentException("cannot " + action + ": " + name);
    }
    return name;
  }

  /** Returns true if the given name is "." or "..". */
  private static boolean isReserved(Name name) {
    // all "." and ".." names are canonicalized to the same objects, so we can use identity
    return name == Name.SELF || name == Name.PARENT;
  }

  // Simple hash table code to avoid allocation of Map.Entry objects when DirectoryEntry can
  // serve the same purpose.

  private static final int INITIAL_CAPACITY = 16;
  private static final int INITIAL_RESIZE_THRESHOLD = (int) (INITIAL_CAPACITY * 0.75);

  private DirectoryEntry[] table = new DirectoryEntry[INITIAL_CAPACITY];
  private int resizeThreshold = INITIAL_RESIZE_THRESHOLD;

  private int entryCount;

  /** Returns the index of the bucket in the array where an entry for the given name should go. */
  private static int bucketIndex(Name name, int tableLength) {
    return name.hashCode() & (tableLength - 1);
  }

  /**
   * Adds the given entry to the directory.
   *
   * @throws IllegalArgumentException if an entry with the given entry's name already exists in the
   *     directory
   */
  @VisibleForTesting
  void put(DirectoryEntry entry) {
    put(entry, false);
  }

  /**
   * Adds the given entry to the directory. {@code overwriteExisting} determines whether an existing
   * entry with the same name should be overwritten or an exception should be thrown.
   */
  private void put(DirectoryEntry entry, boolean overwriteExisting) {
    int index = bucketIndex(entry.name(), table.length);

    // find the place the new entry should go, ensuring an entry with the same name doesn't already
    // exist along the way
    DirectoryEntry prev = null;
    DirectoryEntry curr = table[index];
    while (curr != null) {
      if (curr.name().equals(entry.name())) {
        if (overwriteExisting) {
          // just replace the existing entry; no need to expand, and entryCount doesn't change
          if (prev != null) {
            prev.next = entry;
          } else {
            table[index] = entry;
          }
          entry.next = curr.next;
          curr.next = null;
          entry.file().incrementLinkCount();
          return;
        } else {
          throw new IllegalArgumentException("entry '" + entry.name() + "' already exists");
        }
      }

      prev = curr;
      curr = curr.next;
    }

    entryCount++;
    if (expandIfNeeded()) {
      // if the table was expanded, the index/entry we found is no longer applicable, so just add
      // the entry normally
      index = bucketIndex(entry.name(), table.length);
      addToBucket(index, table, entry);
    } else {
      // otherwise, we just can use the index/entry we found
      if (prev != null) {
        prev.next = entry;
      } else {
        table[index] = entry;
      }
    }

    entry.file().incrementLinkCount();
  }

  /**
   * Adds the given entry to the directory, overwriting an existing entry with the same name if such
   * an entry exists.
   */
  private void forcePut(DirectoryEntry entry) {
    put(entry, true);
  }

  private boolean expandIfNeeded() {
    if (entryCount <= resizeThreshold) {
      return false;
    }

    DirectoryEntry[] newTable = new DirectoryEntry[table.length << 1];

    // redistribute all current entries in the new table
    for (DirectoryEntry entry : table) {
      while (entry != null) {
        int index = bucketIndex(entry.name(), newTable.length);
        addToBucket(index, newTable, entry);
        DirectoryEntry next = entry.next;
        // set entry.next to null; it's always the last entry in its bucket after being added
        entry.next = null;
        entry = next;
      }
    }

    this.table = newTable;
    resizeThreshold <<= 1;
    return true;
  }

  private static void addToBucket(
      int bucketIndex, DirectoryEntry[] table, DirectoryEntry entryToAdd) {
    DirectoryEntry prev = null;
    DirectoryEntry existing = table[bucketIndex];
    while (existing != null) {
      prev = existing;
      existing = existing.next;
    }

    if (prev != null) {
      prev.next = entryToAdd;
    } else {
      table[bucketIndex] = entryToAdd;
    }
  }

  /**
   * Removes and returns the entry for the given name from the directory.
   *
   * @throws IllegalArgumentException if there is no entry with the given name in the directory
   */
  @VisibleForTesting
  DirectoryEntry remove(Name name) {
    int index = bucketIndex(name, table.length);

    DirectoryEntry prev = null;
    DirectoryEntry entry = table[index];
    while (entry != null) {
      if (name.equals(entry.name())) {
        if (prev != null) {
          prev.next = entry.next;
        } else {
          table[index] = entry.next;
        }

        entry.next = null;
        entryCount--;
        entry.file().decrementLinkCount();
        return entry;
      }

      prev = entry;
      entry = entry.next;
    }

    throw new IllegalArgumentException("no entry matching '" + name + "' in this directory");
  }

  @Override
  public Iterator<DirectoryEntry> iterator() {
    return new AbstractIterator<DirectoryEntry>() {
      int index;
      @NullableDecl DirectoryEntry entry;

      @Override
      protected DirectoryEntry computeNext() {
        if (entry != null) {
          entry = entry.next;
        }

        while (entry == null && index < table.length) {
          entry = table[index++];
        }

        return entry != null ? entry : endOfData();
      }
    };
  }
}