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 de.waldheinz.fs.AbstractFsObject;
23 import de.waldheinz.fs.FsDirectory;
24 import de.waldheinz.fs.FsDirectoryEntry;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.HashSet;
29 import java.util.LinkedHashMap;
30 import java.util.Iterator;
31 import java.util.Map;
32 import java.util.Set;
33 
34 /**
35  * The {@link FsDirectory} implementation for FAT file systems. This
36  * implementation aims to fully comply to the FAT specification, including
37  * the quite complex naming system regarding the long file names (LFNs) and
38  * their corresponding 8+3 short file names. This also means that an
39  * {@code FatLfnDirectory} is case-preserving but <em>not</em> case-sensitive.
40  *
41  * @author gbin
42  * @author Matthias Treydte &lt;waldheinz at gmail.com&gt;
43  * @since 0.6
44  */
45 public final class FatLfnDirectory
46         extends AbstractFsObject
47         implements FsDirectory {
48 
49     /**
50      * This set is used to check if a file name is already in use in this
51      * directory. The FAT specification says that file names must be unique
52      * ignoring the case, so this set contains all names converted to
53      * lower-case, and all checks must be performed using lower-case strings.
54      */
55     private final Set<String> usedNames;
56     private final Fat fat;
57     private final Map<ShortName, FatLfnDirectoryEntry> shortNameIndex;
58     private final Map<String, FatLfnDirectoryEntry> longNameIndex;
59     private final Map<FatDirectoryEntry, FatFile> entryToFile;
60     private final Map<FatDirectoryEntry, FatLfnDirectory> entryToDirectory;
61     private Dummy83BufferGenerator dbg;
62 
63     final AbstractDirectory dir;
64 
FatLfnDirectory(AbstractDirectory dir, Fat fat, boolean readOnly)65     FatLfnDirectory(AbstractDirectory dir, Fat fat, boolean readOnly)
66             throws IOException {
67 
68         super(readOnly);
69 
70         if ((dir == null) || (fat == null)) throw new NullPointerException();
71 
72         this.fat = fat;
73         this.dir = dir;
74 
75         this.shortNameIndex =
76                 new LinkedHashMap<ShortName, FatLfnDirectoryEntry>();
77 
78         this.longNameIndex =
79                 new LinkedHashMap<String, FatLfnDirectoryEntry>();
80 
81         this.entryToFile =
82                 new LinkedHashMap<FatDirectoryEntry, FatFile>();
83 
84         this.entryToDirectory =
85                 new LinkedHashMap<FatDirectoryEntry, FatLfnDirectory>();
86 
87         this.usedNames = new HashSet<String>();
88         this.dbg = new Dummy83BufferGenerator();
89 
90         parseLfn();
91     }
92 
getFile(FatDirectoryEntry entry)93     FatFile getFile(FatDirectoryEntry entry) throws IOException {
94         FatFile file = entryToFile.get(entry);
95 
96         if (file == null) {
97             file = FatFile.get(fat, entry);
98             entryToFile.put(entry, file);
99         }
100 
101         return file;
102     }
103 
getDirectory(FatDirectoryEntry entry)104     FatLfnDirectory getDirectory(FatDirectoryEntry entry) throws IOException {
105         FatLfnDirectory result = entryToDirectory.get(entry);
106 
107         if (result == null) {
108             final ClusterChainDirectory storage = read(entry, fat);
109             result = new FatLfnDirectory(storage, fat, isReadOnly());
110             entryToDirectory.put(entry, result);
111         }
112 
113         return result;
114     }
115 
116     /**
117      * <p>
118      * {@inheritDoc}
119      * </p><p>
120      * According to the FAT file system specification, leading and trailing
121      * spaces in the {@code name} are ignored by this method.
122      * </p>
123      *
124      * @param name {@inheritDoc}
125      * @return {@inheritDoc}
126      * @throws IOException {@inheritDoc}
127      */
128     @Override
addFile(String name)129     public FatLfnDirectoryEntry addFile(String name) throws IOException {
130         checkWritable();
131         checkUniqueName(name);
132 
133         name = name.trim();
134         final ShortName sn = makeShortName(name, false);
135 
136         final FatLfnDirectoryEntry entry =
137                 new FatLfnDirectoryEntry(name, sn, this, false);
138 
139         dir.addEntries(entry.compactForm());
140 
141         shortNameIndex.put(sn, entry);
142         longNameIndex.put(name.toLowerCase(), entry);
143 
144         getFile(entry.realEntry);
145 
146         dir.setDirty();
147         return entry;
148     }
149 
isFreeName(String name)150     boolean isFreeName(String name) {
151         return true;
152     }
153 
checkUniqueName(String name)154     private void checkUniqueName(String name) throws IOException {
155     }
156 
freeUniqueName(String name)157     private void freeUniqueName(String name) {
158     }
159 
makeShortName(String name, boolean isDirectory)160     private ShortName makeShortName(String name, boolean isDirectory) throws IOException {
161         final ShortName result;
162 
163         try {
164             result = dbg.generate83BufferNew(name);
165         } catch (IllegalArgumentException ex) {
166             throw new IOException(
167                     "could not generate short name for \"" + name + "\"", ex);
168         }
169         return result;
170     }
171 
172     /**
173      * <p>
174      * {@inheritDoc}
175      * </p><p>
176      * According to the FAT file system specification, leading and trailing
177      * spaces in the {@code name} are ignored by this method.
178      * </p>
179      *
180      * @param name {@inheritDoc}
181      * @return {@inheritDoc}
182      * @throws IOException {@inheritDoc}
183      */
184     @Override
addDirectory(String name)185     public FatLfnDirectoryEntry addDirectory(String name) throws IOException {
186         checkWritable();
187         checkUniqueName(name);
188 
189         name = name.trim();
190         final ShortName sn = makeShortName(name, true);
191         final FatDirectoryEntry real = dir.createSub(fat);
192         real.setShortName(sn);
193         final FatLfnDirectoryEntry e =
194                 new FatLfnDirectoryEntry(this, real, name);
195 
196         try {
197             dir.addEntries(e.compactForm());
198         } catch (IOException ex) {
199             final ClusterChain cc =
200                     new ClusterChain(fat, real.getStartCluster(), false);
201             cc.setChainLength(0);
202             dir.removeEntry(real);
203             throw ex;
204         }
205 
206         shortNameIndex.put(sn, e);
207         longNameIndex.put(name.toLowerCase(), e);
208 
209         getDirectory(real);
210 
211         flush();
212         return e;
213     }
214 
215     /**
216      * <p>
217      * {@inheritDoc}
218      * </p><p>
219      * According to the FAT file system specification, leading and trailing
220      * spaces in the {@code name} are ignored by this method.
221      * </p>
222      *
223      * @param name {@inheritDoc}
224      * @return {@inheritDoc}
225      */
226     @Override
getEntry(String name)227     public FatLfnDirectoryEntry getEntry(String name) {
228         name = name.trim().toLowerCase();
229 
230         final FatLfnDirectoryEntry entry = longNameIndex.get(name);
231 
232         if (entry == null) {
233             if (!ShortName.canConvert(name)) return null;
234             return shortNameIndex.get(ShortName.get(name));
235         } else {
236             return entry;
237         }
238     }
239 
parseLfn()240     private void parseLfn() throws IOException {
241         int i = 0;
242         final int size = dir.getEntryCount();
243 
244         while (i < size) {
245             // jump over empty entries
246             while (i < size && dir.getEntry(i) == null) {
247                 i++;
248             }
249 
250             if (i >= size) {
251                 break;
252             }
253 
254             int offset = i; // beginning of the entry
255             // check when we reach a real entry
256             while (dir.getEntry(i).isLfnEntry()) {
257                 i++;
258                 if (i >= size) {
259                     // This is a cutted entry, forgive it
260                     break;
261                 }
262             }
263 
264             if (i >= size) {
265                 // This is a cutted entry, forgive it
266                 break;
267             }
268 
269             final FatLfnDirectoryEntry current =
270                     FatLfnDirectoryEntry.extract(this, offset, ++i - offset);
271 
272             if (!current.realEntry.isDeleted() && current.isValid()) {
273                 checkUniqueName(current.getName());
274 
275                 shortNameIndex.put(current.realEntry.getShortName(), current);
276                 longNameIndex.put(current.getName().toLowerCase(), current);
277             }
278         }
279     }
280 
updateLFN()281     private void updateLFN() throws IOException {
282         ArrayList<FatDirectoryEntry> dest =
283                 new ArrayList<FatDirectoryEntry>();
284 
285         for (FatLfnDirectoryEntry currentEntry : shortNameIndex.values()) {
286             FatDirectoryEntry[] encoded = currentEntry.compactForm();
287             dest.addAll(Arrays.asList(encoded));
288         }
289 
290         final int size = dest.size();
291 
292         dir.changeSize(size);
293         dir.setEntries(dest);
294     }
295 
296     @Override
flush()297     public void flush() throws IOException {
298         checkWritable();
299 
300         for (FatFile f : entryToFile.values()) {
301             f.flush();
302         }
303 
304         for (FatLfnDirectory d : entryToDirectory.values()) {
305             d.flush();
306         }
307 
308         updateLFN();
309         dir.flush();
310     }
311 
312     @Override
iterator()313     public Iterator<FsDirectoryEntry> iterator() {
314         return new Iterator<FsDirectoryEntry>() {
315 
316             final Iterator<FatLfnDirectoryEntry> it =
317                     shortNameIndex.values().iterator();
318 
319             @Override
320             public boolean hasNext() {
321                 return it.hasNext();
322             }
323 
324             @Override
325             public FsDirectoryEntry next() {
326                 return it.next();
327             }
328 
329             /**
330              * @see java.util.Iterator#remove()
331              */
332             @Override
333             public void remove() {
334                 throw new UnsupportedOperationException();
335             }
336         };
337     }
338 
339     /**
340      * Remove the entry with the given name from this directory.
341      *
342      * @param name the name of the entry to remove
343      * @throws IOException on error removing the entry
344      * @throws IllegalArgumentException on an attempt to remove the dot entries
345      */
346     @Override
remove(String name)347     public void remove(String name)
348             throws IOException, IllegalArgumentException {
349 
350         checkWritable();
351 
352         final FatLfnDirectoryEntry entry = getEntry(name);
353         if (entry == null) return;
354 
355         unlinkEntry(entry);
356 
357         final ClusterChain cc = new ClusterChain(
358                 fat, entry.realEntry.getStartCluster(), false);
359 
360         cc.setChainLength(0);
361 
362         freeUniqueName(name);
363         updateLFN();
364     }
365 
366     /**
367      * Unlinks the specified entry from this directory without actually
368      * deleting it.
369      *
370      * @param e the entry to be unlinked
371      * @see #linkEntry(de.waldheinz.fs.fat.FatLfnDirectoryEntry)
372      */
unlinkEntry(FatLfnDirectoryEntry entry)373     void unlinkEntry(FatLfnDirectoryEntry entry) {
374         final ShortName sn = entry.realEntry.getShortName();
375 
376         if (sn.equals(ShortName.DOT) || sn.equals(ShortName.DOT_DOT)) throw
377                 new IllegalArgumentException(
378                     "the dot entries can not be removed");
379 
380         final String lowerName = entry.getName().toLowerCase();
381 
382         assert (this.longNameIndex.containsKey(lowerName));
383         this.longNameIndex.remove(lowerName);
384 
385         assert (this.shortNameIndex.containsKey(sn));
386         this.shortNameIndex.remove(sn);
387 
388         if (entry.isFile()) {
389             this.entryToFile.remove(entry.realEntry);
390         } else {
391             this.entryToDirectory.remove(entry.realEntry);
392         }
393     }
394 
395     /**
396      * Links the specified entry to this directory, updating the entrie's
397      * short name.
398      *
399      * @param entry the entry to be linked (added) to this directory
400      * @see #unlinkEntry(de.waldheinz.fs.fat.FatLfnDirectoryEntry)
401      */
linkEntry(FatLfnDirectoryEntry entry)402     void linkEntry(FatLfnDirectoryEntry entry) throws IOException {
403         checkUniqueName(entry.getName());
404         ShortName name;
405         name = this.dbg.generate83BufferNew(entry.getName());
406         entry.realEntry.setShortName(name);
407 
408         this.longNameIndex.put(entry.getName().toLowerCase(), entry);
409         this.shortNameIndex.put(entry.realEntry.getShortName(), entry);
410 
411         updateLFN();
412     }
413 
414     @Override
toString()415     public String toString() {
416         return getClass().getSimpleName() +
417                 " [size=" + shortNameIndex.size() + //NOI18N
418                 ", dir=" + dir + "]"; //NOI18N
419     }
420 
read(FatDirectoryEntry entry, Fat fat)421     private static ClusterChainDirectory read(FatDirectoryEntry entry, Fat fat)
422             throws IOException {
423 
424         if (!entry.isDirectory()) throw
425                 new IllegalArgumentException(entry + " is no directory");
426 
427         final ClusterChain chain = new ClusterChain(
428                 fat, entry.getStartCluster(),
429                 entry.isReadonlyFlag());
430 
431         final ClusterChainDirectory result =
432                 new ClusterChainDirectory(chain, false);
433 
434         result.read();
435         return result;
436     }
437 
438 }
439