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 <epr at jnode.org> 32 * @author Matthias Treydte <waldheinz at gmail.com> 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