summaryrefslogtreecommitdiff
path: root/src/main/java/de/waldheinz/fs/fat/AbstractDirectory.java
blob: d3445b78c86716353b303fa0534953de65d4edab (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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
/*
 * Copyright (C) 2003-2009 JNode.org
 *               2009,2010 Matthias Treydte <mt@waldheinz.de>
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; If not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
 
package de.waldheinz.fs.fat;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This is the abstract base class for all directory implementations.
 *
 * @author Ewout Prangsma &lt;epr at jnode.org&gt;
 * @author Matthias Treydte &lt;waldheinz at gmail.com&gt;
 */
abstract class AbstractDirectory {

    /**
     * The maximum length of the volume label.
     *
     * @see #setLabel(java.lang.String) 
     */
    public static final int MAX_LABEL_LENGTH = 11;
    
    private final List<FatDirectoryEntry> entries;
    private final boolean readOnly;
    private final boolean isRoot;
    
    private boolean dirty;
    private int capacity;
    private String volumeLabel;

    /**
     * Creates a new instance of {@code AbstractDirectory}.
     *
     * @param capacity the initial capacity of the new instance
     * @param readOnly if the instance should be read-only
     * @param isRoot if the new {@code AbstractDirectory} represents a root
     *      directory
     */
    protected AbstractDirectory(
            int capacity, boolean readOnly, boolean isRoot) {
        
        this.entries = new ArrayList<FatDirectoryEntry>();
        this.capacity = capacity;
        this.readOnly = readOnly;
        this.isRoot = isRoot;
    }

    /**
     * Gets called when the {@code AbstractDirectory} must read it's content
     * off the backing storage. This method must always fill the buffer's
     * remaining space with the bytes making up this directory, beginning with
     * the first byte.
     *
     * @param data the {@code ByteBuffer} to fill
     * @throws IOException on read error
     */
    protected abstract void read(ByteBuffer data) throws IOException;

    /**
     * Gets called when the {@code AbstractDirectory} wants to write it's
     * contents to the backing storage. This method is expected to write the
     * buffer's remaining data to the storage, beginning with the first byte.
     *
     * @param data the {@code ByteBuffer} to write
     * @throws IOException on write error
     */
    protected abstract void write(ByteBuffer data) throws IOException;

    /**
     * Returns the number of the cluster where this directory is stored. This
     * is important when creating the ".." entry in a sub-directory, as this
     * entry must poing to the storage cluster of it's parent.
     *
     * @return this directory's storage cluster
     */
    protected abstract long getStorageCluster();

    /**
     * Gets called by the {@code AbstractDirectory} when it has determined that
     * it should resize because the number of entries has changed.
     *
     * @param entryCount the new number of entries this directory needs to store
     * @throws IOException on write error
     * @throws DirectoryFullException if the FAT12/16 root directory is full
     * @see #sizeChanged(long)
     * @see #checkEntryCount(int) 
     */
    protected abstract void changeSize(int entryCount)
            throws DirectoryFullException, IOException;
            
    /**
     * Replaces all entries in this directory.
     *
     * @param newEntries the new directory entries
     */
    public void setEntries(List<FatDirectoryEntry> newEntries) {
        if (newEntries.size() > capacity)
            throw new IllegalArgumentException("too many entries");
        
        this.entries.clear();
        this.entries.addAll(newEntries);
    }
    
    /**
     * 
     *
     * @param newSize the new storage space for the directory in bytes
     * @see #changeSize(int) 
     */
    protected final void sizeChanged(long newSize) throws IOException {
        final long newCount = newSize / FatDirectoryEntry.SIZE;
        if (newCount > Integer.MAX_VALUE)
            throw new IOException("directory too large");
        
        this.capacity = (int) newCount;
    }

    public final FatDirectoryEntry getEntry(int idx) {
        return this.entries.get(idx);
    }
    
    /**
     * Returns the current capacity of this {@code AbstractDirectory}.
     *
     * @return the number of entries this directory can hold in its current
     *      storage space
     * @see #changeSize(int)
     */
    public final int getCapacity() {
        return this.capacity;
    }

    /**
     * The number of entries that are currently stored in this
     * {@code AbstractDirectory}.
     *
     * @return the current number of directory entries
     */
    public final int getEntryCount() {
        return this.entries.size();
    }
    
    public boolean isReadOnly() {
        return readOnly;
    }

    public final boolean isRoot() {
        return this.isRoot;
    }
    
    /**
     * Gets the number of directory entries in this directory. This is the
     * number of "real" entries in this directory, possibly plus one if a
     * volume label is set.
     * 
     * @return the number of entries in this directory
     */
    public int getSize() {
        return entries.size() + ((this.volumeLabel != null) ? 1 : 0);
    }
    
    /**
     * Mark this directory as dirty.
     */
    protected final void setDirty() {
        this.dirty = true;
    }

    /**
     * Checks if this {@code AbstractDirectory} is a root directory.
     *
     * @throws UnsupportedOperationException if this is not a root directory
     * @see #isRoot() 
     */
    private void checkRoot() throws UnsupportedOperationException {
        if (!isRoot()) {
            throw new UnsupportedOperationException(
                    "only supported on root directories");
        }
    }
    
    /**
     * Mark this directory as not dirty.
     */
    private void resetDirty() {
        this.dirty = false;
    }
    
    /**
     * Flush the contents of this directory to the persistent storage
     */
    public void flush() throws IOException {
        
        final ByteBuffer data = ByteBuffer.allocate(
                getCapacity() * FatDirectoryEntry.SIZE);
        
        for (int i=0; i < entries.size(); i++) {
            final FatDirectoryEntry entry = entries.get(i);
            
            if (entry != null) {
                entry.write(data);
            }
        }
        
        /* TODO: the label could be placed directly the dot entries */
        
        if (this.volumeLabel != null) {
            final FatDirectoryEntry labelEntry =
                    FatDirectoryEntry.createVolumeLabel(volumeLabel);

            labelEntry.write(data);
        }
        
        if (data.hasRemaining()) {
            FatDirectoryEntry.writeNullEntry(data);
        }

        data.flip();
        
        write(data);
        resetDirty();
    }
    
    protected final void read() throws IOException {
        final ByteBuffer data = ByteBuffer.allocate(
                getCapacity() * FatDirectoryEntry.SIZE);
                
        read(data);
        data.flip();
        
        for (int i=0; i < getCapacity(); i++) {
            final FatDirectoryEntry e =
                    FatDirectoryEntry.read(data, isReadOnly());
            
            if (e == null) break;
            
            if (e.isVolumeLabel()) {
                if (!this.isRoot) throw new IOException(
                        "volume label in non-root directory");
                
                this.volumeLabel = e.getVolumeLabel();
            } else {
                entries.add(e);
            }
        }
    }
    
    public void addEntry(FatDirectoryEntry e) throws IOException {
        assert (e != null);
        
        if (getSize() == getCapacity()) {
            changeSize(getCapacity() + 1);
        }

        entries.add(e);
    }
    
    public void addEntries(FatDirectoryEntry[] entries)
            throws IOException {
        
        if (getSize() + entries.length > getCapacity()) {
            changeSize(getSize() + entries.length);
        }

        this.entries.addAll(Arrays.asList(entries));
    }
    
    public void removeEntry(FatDirectoryEntry entry) throws IOException {
        assert (entry != null);
        
        this.entries.remove(entry);
        changeSize(getSize());
    }

    /**
     * Returns the volume label that is stored in this directory. Reading the
     * volume label is only supported for the root directory.
     *
     * @return the volume label stored in this directory, or {@code null}
     * @throws UnsupportedOperationException if this is not a root directory
     * @see #isRoot() 
     */
    public String getLabel() throws UnsupportedOperationException {
        checkRoot();
        
        return volumeLabel;
    }

    public FatDirectoryEntry createSub(Fat fat) throws IOException {
        final ClusterChain chain = new ClusterChain(fat, false);
        chain.setChainLength(1);

        final FatDirectoryEntry entry = FatDirectoryEntry.create(true);
        entry.setStartCluster(chain.getStartCluster());
        
        final ClusterChainDirectory dir =
                new ClusterChainDirectory(chain, false);

        /* add "." entry */

        final FatDirectoryEntry dot = FatDirectoryEntry.create(true);
        dot.setShortName(ShortName.DOT);
        dot.setStartCluster(dir.getStorageCluster());
        copyDateTimeFields(entry, dot);
        dir.addEntry(dot);

        /* add ".." entry */

        final FatDirectoryEntry dotDot = FatDirectoryEntry.create(true);
        dotDot.setShortName(ShortName.DOT_DOT);
        dotDot.setStartCluster(getStorageCluster());
        copyDateTimeFields(entry, dotDot);
        dir.addEntry(dotDot);

        dir.flush();

        return entry;
    }
    
    private static void copyDateTimeFields(
            FatDirectoryEntry src, FatDirectoryEntry dst) {
        
        dst.setCreated(src.getCreated());
        dst.setLastAccessed(src.getLastAccessed());
        dst.setLastModified(src.getLastModified());
    }

    /**
     * Sets the volume label that is stored in this directory. Setting the
     * volume label is supported on the root directory only.
     *
     * @param label the new volume label
     * @throws IllegalArgumentException if the label is too long
     * @throws UnsupportedOperationException if this is not a root directory
     * @see #isRoot() 
     */
    public void setLabel(String label) throws IllegalArgumentException,
            UnsupportedOperationException, IOException {

        checkRoot();

        if (label.length() > MAX_LABEL_LENGTH) throw new
                IllegalArgumentException("label too long");

        if (this.volumeLabel != null) {
            if (label == null) {
                changeSize(getSize() - 1);
                this.volumeLabel = null;
            } else {
                ShortName.checkValidChars(label.toCharArray());
                this.volumeLabel = label;
            }
        } else {
            if (label != null) {
                changeSize(getSize() + 1);
                ShortName.checkValidChars(label.toCharArray());
                this.volumeLabel = label;
            }
        }

        this.dirty = true;
    }
    
}