1 /*
2  * Copyright (C) 2003-2009 JNode.org
3  *               2009,2010 Matthias Treydte <mt@waldheinz.de>
4  *
5  * This library is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as published
7  * by the Free Software Foundation; either version 2.1 of the License, or
8  * (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13  * License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this library; If not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 package de.waldheinz.fs.fat;
21 
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.List;
27 
28 /**
29  * This is the abstract base class for all directory implementations.
30  *
31  * @author Ewout Prangsma &lt;epr at jnode.org&gt;
32  * @author Matthias Treydte &lt;waldheinz at gmail.com&gt;
33  */
34 abstract class AbstractDirectory {
35 
36     /**
37      * The maximum length of the volume label.
38      *
39      * @see #setLabel(java.lang.String)
40      */
41     public static final int MAX_LABEL_LENGTH = 11;
42 
43     private final List<FatDirectoryEntry> entries;
44     private final boolean readOnly;
45     private final boolean isRoot;
46 
47     private boolean dirty;
48     private int capacity;
49     private String volumeLabel;
50 
51     /**
52      * Creates a new instance of {@code AbstractDirectory}.
53      *
54      * @param capacity the initial capacity of the new instance
55      * @param readOnly if the instance should be read-only
56      * @param isRoot if the new {@code AbstractDirectory} represents a root
57      *      directory
58      */
AbstractDirectory( int capacity, boolean readOnly, boolean isRoot)59     protected AbstractDirectory(
60             int capacity, boolean readOnly, boolean isRoot) {
61 
62         this.entries = new ArrayList<FatDirectoryEntry>();
63         this.capacity = capacity;
64         this.readOnly = readOnly;
65         this.isRoot = isRoot;
66     }
67 
68     /**
69      * Gets called when the {@code AbstractDirectory} must read it's content
70      * off the backing storage. This method must always fill the buffer's
71      * remaining space with the bytes making up this directory, beginning with
72      * the first byte.
73      *
74      * @param data the {@code ByteBuffer} to fill
75      * @throws IOException on read error
76      */
read(ByteBuffer data)77     protected abstract void read(ByteBuffer data) throws IOException;
78 
79     /**
80      * Gets called when the {@code AbstractDirectory} wants to write it's
81      * contents to the backing storage. This method is expected to write the
82      * buffer's remaining data to the storage, beginning with the first byte.
83      *
84      * @param data the {@code ByteBuffer} to write
85      * @throws IOException on write error
86      */
write(ByteBuffer data)87     protected abstract void write(ByteBuffer data) throws IOException;
88 
89     /**
90      * Returns the number of the cluster where this directory is stored. This
91      * is important when creating the ".." entry in a sub-directory, as this
92      * entry must poing to the storage cluster of it's parent.
93      *
94      * @return this directory's storage cluster
95      */
getStorageCluster()96     protected abstract long getStorageCluster();
97 
98     /**
99      * Gets called by the {@code AbstractDirectory} when it has determined that
100      * it should resize because the number of entries has changed.
101      *
102      * @param entryCount the new number of entries this directory needs to store
103      * @throws IOException on write error
104      * @throws DirectoryFullException if the FAT12/16 root directory is full
105      * @see #sizeChanged(long)
106      * @see #checkEntryCount(int)
107      */
changeSize(int entryCount)108     protected abstract void changeSize(int entryCount)
109             throws DirectoryFullException, IOException;
110 
111     /**
112      * Replaces all entries in this directory.
113      *
114      * @param newEntries the new directory entries
115      */
setEntries(List<FatDirectoryEntry> newEntries)116     public void setEntries(List<FatDirectoryEntry> newEntries) {
117         if (newEntries.size() > capacity)
118             throw new IllegalArgumentException("too many entries");
119 
120         this.entries.clear();
121         this.entries.addAll(newEntries);
122     }
123 
124     /**
125      *
126      *
127      * @param newSize the new storage space for the directory in bytes
128      * @see #changeSize(int)
129      */
sizeChanged(long newSize)130     protected final void sizeChanged(long newSize) throws IOException {
131         final long newCount = newSize / FatDirectoryEntry.SIZE;
132         if (newCount > Integer.MAX_VALUE)
133             throw new IOException("directory too large");
134 
135         this.capacity = (int) newCount;
136     }
137 
getEntry(int idx)138     public final FatDirectoryEntry getEntry(int idx) {
139         return this.entries.get(idx);
140     }
141 
142     /**
143      * Returns the current capacity of this {@code AbstractDirectory}.
144      *
145      * @return the number of entries this directory can hold in its current
146      *      storage space
147      * @see #changeSize(int)
148      */
getCapacity()149     public final int getCapacity() {
150         return this.capacity;
151     }
152 
153     /**
154      * The number of entries that are currently stored in this
155      * {@code AbstractDirectory}.
156      *
157      * @return the current number of directory entries
158      */
getEntryCount()159     public final int getEntryCount() {
160         return this.entries.size();
161     }
162 
isReadOnly()163     public boolean isReadOnly() {
164         return readOnly;
165     }
166 
isRoot()167     public final boolean isRoot() {
168         return this.isRoot;
169     }
170 
171     /**
172      * Gets the number of directory entries in this directory. This is the
173      * number of "real" entries in this directory, possibly plus one if a
174      * volume label is set.
175      *
176      * @return the number of entries in this directory
177      */
getSize()178     public int getSize() {
179         return entries.size() + ((this.volumeLabel != null) ? 1 : 0);
180     }
181 
182     /**
183      * Mark this directory as dirty.
184      */
setDirty()185     protected final void setDirty() {
186         this.dirty = true;
187     }
188 
189     /**
190      * Checks if this {@code AbstractDirectory} is a root directory.
191      *
192      * @throws UnsupportedOperationException if this is not a root directory
193      * @see #isRoot()
194      */
checkRoot()195     private void checkRoot() throws UnsupportedOperationException {
196         if (!isRoot()) {
197             throw new UnsupportedOperationException(
198                     "only supported on root directories");
199         }
200     }
201 
202     /**
203      * Mark this directory as not dirty.
204      */
resetDirty()205     private void resetDirty() {
206         this.dirty = false;
207     }
208 
209     /**
210      * Flush the contents of this directory to the persistent storage
211      */
flush()212     public void flush() throws IOException {
213 
214         final ByteBuffer data = ByteBuffer.allocate(
215                 getCapacity() * FatDirectoryEntry.SIZE);
216 
217         for (int i=0; i < entries.size(); i++) {
218             final FatDirectoryEntry entry = entries.get(i);
219 
220             if (entry != null) {
221                 entry.write(data);
222             }
223         }
224 
225         /* TODO: the label could be placed directly the dot entries */
226 
227         if (this.volumeLabel != null) {
228             final FatDirectoryEntry labelEntry =
229                     FatDirectoryEntry.createVolumeLabel(volumeLabel);
230 
231             labelEntry.write(data);
232         }
233 
234         if (data.hasRemaining()) {
235             FatDirectoryEntry.writeNullEntry(data);
236         }
237 
238         data.flip();
239 
240         write(data);
241         resetDirty();
242     }
243 
read()244     protected final void read() throws IOException {
245         final ByteBuffer data = ByteBuffer.allocate(
246                 getCapacity() * FatDirectoryEntry.SIZE);
247 
248         read(data);
249         data.flip();
250 
251         for (int i=0; i < getCapacity(); i++) {
252             final FatDirectoryEntry e =
253                     FatDirectoryEntry.read(data, isReadOnly());
254 
255             if (e == null) break;
256 
257             if (e.isVolumeLabel()) {
258                 if (!this.isRoot) throw new IOException(
259                         "volume label in non-root directory");
260 
261                 this.volumeLabel = e.getVolumeLabel();
262             } else {
263                 entries.add(e);
264             }
265         }
266     }
267 
addEntry(FatDirectoryEntry e)268     public void addEntry(FatDirectoryEntry e) throws IOException {
269         assert (e != null);
270 
271         if (getSize() == getCapacity()) {
272             changeSize(getCapacity() + 1);
273         }
274 
275         entries.add(e);
276     }
277 
addEntries(FatDirectoryEntry[] entries)278     public void addEntries(FatDirectoryEntry[] entries)
279             throws IOException {
280 
281         if (getSize() + entries.length > getCapacity()) {
282             changeSize(getSize() + entries.length);
283         }
284 
285         this.entries.addAll(Arrays.asList(entries));
286     }
287 
removeEntry(FatDirectoryEntry entry)288     public void removeEntry(FatDirectoryEntry entry) throws IOException {
289         assert (entry != null);
290 
291         this.entries.remove(entry);
292         changeSize(getSize());
293     }
294 
295     /**
296      * Returns the volume label that is stored in this directory. Reading the
297      * volume label is only supported for the root directory.
298      *
299      * @return the volume label stored in this directory, or {@code null}
300      * @throws UnsupportedOperationException if this is not a root directory
301      * @see #isRoot()
302      */
getLabel()303     public String getLabel() throws UnsupportedOperationException {
304         checkRoot();
305 
306         return volumeLabel;
307     }
308 
createSub(Fat fat)309     public FatDirectoryEntry createSub(Fat fat) throws IOException {
310         final ClusterChain chain = new ClusterChain(fat, false);
311         chain.setChainLength(1);
312 
313         final FatDirectoryEntry entry = FatDirectoryEntry.create(true);
314         entry.setStartCluster(chain.getStartCluster());
315 
316         final ClusterChainDirectory dir =
317                 new ClusterChainDirectory(chain, false);
318 
319         /* add "." entry */
320 
321         final FatDirectoryEntry dot = FatDirectoryEntry.create(true);
322         dot.setShortName(ShortName.DOT);
323         dot.setStartCluster(dir.getStorageCluster());
324         copyDateTimeFields(entry, dot);
325         dir.addEntry(dot);
326 
327         /* add ".." entry */
328 
329         final FatDirectoryEntry dotDot = FatDirectoryEntry.create(true);
330         dotDot.setShortName(ShortName.DOT_DOT);
331         dotDot.setStartCluster(getStorageCluster());
332         copyDateTimeFields(entry, dotDot);
333         dir.addEntry(dotDot);
334 
335         dir.flush();
336 
337         return entry;
338     }
339 
copyDateTimeFields( FatDirectoryEntry src, FatDirectoryEntry dst)340     private static void copyDateTimeFields(
341             FatDirectoryEntry src, FatDirectoryEntry dst) {
342 
343         dst.setCreated(src.getCreated());
344         dst.setLastAccessed(src.getLastAccessed());
345         dst.setLastModified(src.getLastModified());
346     }
347 
348     /**
349      * Sets the volume label that is stored in this directory. Setting the
350      * volume label is supported on the root directory only.
351      *
352      * @param label the new volume label
353      * @throws IllegalArgumentException if the label is too long
354      * @throws UnsupportedOperationException if this is not a root directory
355      * @see #isRoot()
356      */
setLabel(String label)357     public void setLabel(String label) throws IllegalArgumentException,
358             UnsupportedOperationException, IOException {
359 
360         checkRoot();
361 
362         if (label.length() > MAX_LABEL_LENGTH) throw new
363                 IllegalArgumentException("label too long");
364 
365         if (this.volumeLabel != null) {
366             if (label == null) {
367                 changeSize(getSize() - 1);
368                 this.volumeLabel = null;
369             } else {
370                 ShortName.checkValidChars(label.toCharArray());
371                 this.volumeLabel = label;
372             }
373         } else {
374             if (label != null) {
375                 changeSize(getSize() + 1);
376                 ShortName.checkValidChars(label.toCharArray());
377                 this.volumeLabel = label;
378             }
379         }
380 
381         this.dirty = true;
382     }
383 
384 }
385