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 java.nio.ByteBuffer;
24 
25 /**
26  *
27  *
28  * @author Ewout Prangsma &lt;epr at jnode.org&gt;
29  * @author Matthias Treydte &lt;waldheinz at gmail.com&gt;
30  */
31 final class FatDirectoryEntry extends AbstractFsObject {
32 
33     /**
34      * The size in bytes of an FAT directory entry.
35      */
36     public final static int SIZE = 32;
37 
38     /**
39      * The offset to the attributes byte.
40      */
41     private static final int OFFSET_ATTRIBUTES = 0x0b;
42 
43     /**
44      * The offset to the file size dword.
45      */
46     private static final int OFFSET_FILE_SIZE = 0x1c;
47 
48     private static final int F_READONLY = 0x01;
49     private static final int F_HIDDEN = 0x02;
50     private static final int F_SYSTEM = 0x04;
51     private static final int F_VOLUME_ID = 0x08;
52     private static final int F_DIRECTORY = 0x10;
53     private static final int F_ARCHIVE = 0x20;
54 
55     private static final int MAX_CLUSTER = 0xFFFF;
56 
57     /**
58      * The magic byte denoting that this entry was deleted and is free
59      * for reuse.
60      *
61      * @see #isDeleted()
62      */
63     public static final int ENTRY_DELETED_MAGIC = 0xe5;
64 
65     private final byte[] data;
66     private boolean dirty;
67     boolean hasShortNameOnly;
68 
FatDirectoryEntry(byte[] data, boolean readOnly)69     FatDirectoryEntry(byte[] data, boolean readOnly) {
70         super(readOnly);
71 
72         this.data = data;
73     }
74 
FatDirectoryEntry()75     private FatDirectoryEntry() {
76         this(new byte[SIZE], false);
77 
78     }
79 
80     /**
81      * Reads a {@code FatDirectoryEntry} from the specified {@code ByteBuffer}.
82      * The buffer must have at least {@link #SIZE} bytes remaining. The entry
83      * is read from the buffer's current position, and if this method returns
84      * non-null the position will have advanced by {@link #SIZE} bytes,
85      * otherwise the position will remain unchanged.
86      *
87      * @param buff the buffer to read the entry from
88      * @param readOnly if the resulting {@code FatDirecoryEntry} should be
89      *      read-only
90      * @return the directory entry that was read from the buffer or {@code null}
91      *      if there was no entry to read from the specified position (first
92      *      byte was 0)
93      */
read(ByteBuffer buff, boolean readOnly)94     public static FatDirectoryEntry read(ByteBuffer buff, boolean readOnly) {
95         assert (buff.remaining() >= SIZE);
96 
97         /* peek into the buffer to see if we're done with reading */
98 
99         if (buff.get(buff.position()) == 0) return null;
100 
101         /* read the directory entry */
102 
103         final byte[] data = new byte[SIZE];
104         buff.get(data);
105         return new FatDirectoryEntry(data, readOnly);
106     }
107 
writeNullEntry(ByteBuffer buff)108     public static void writeNullEntry(ByteBuffer buff) {
109         for (int i=0; i < SIZE; i++) {
110             buff.put((byte) 0);
111         }
112     }
113 
114     /**
115      * Decides if this entry is a "volume label" entry according to the FAT
116      * specification.
117      *
118      * @return if this is a volume label entry
119      */
isVolumeLabel()120     public boolean isVolumeLabel() {
121         if (isLfnEntry()) return false;
122         else return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_VOLUME_ID);
123     }
124 
setFlag(int mask, boolean set)125     private void setFlag(int mask, boolean set) {
126         final int oldFlags = getFlags();
127 
128         if (((oldFlags & mask) != 0) == set) return;
129 
130         if (set) {
131             setFlags(oldFlags | mask);
132         } else {
133             setFlags(oldFlags & ~mask);
134         }
135 
136         this.dirty = true;
137     }
138 
isSystemFlag()139     public boolean isSystemFlag() {
140         return ((getFlags() & F_SYSTEM) != 0);
141     }
142 
setSystemFlag(boolean isSystem)143     public void setSystemFlag(boolean isSystem) {
144         setFlag(F_SYSTEM, isSystem);
145     }
146 
isArchiveFlag()147     public boolean isArchiveFlag() {
148         return ((getFlags() & F_ARCHIVE) != 0);
149     }
150 
setArchiveFlag(boolean isArchive)151     public void setArchiveFlag(boolean isArchive) {
152         setFlag(F_ARCHIVE, isArchive);
153     }
154 
isHiddenFlag()155     public boolean isHiddenFlag() {
156         return ((getFlags() & F_HIDDEN) != 0);
157     }
158 
setHiddenFlag(boolean isHidden)159     public void setHiddenFlag(boolean isHidden) {
160         setFlag(F_HIDDEN, isHidden);
161     }
162 
isVolumeIdFlag()163     public boolean isVolumeIdFlag() {
164         return ((getFlags() & F_VOLUME_ID) != 0);
165     }
166 
isLfnEntry()167     public boolean isLfnEntry() {
168         return isReadonlyFlag() && isSystemFlag() &&
169                 isHiddenFlag() && isVolumeIdFlag();
170     }
171 
isDirty()172     public boolean isDirty() {
173         return dirty;
174     }
175 
getFlags()176     private int getFlags() {
177         return LittleEndian.getUInt8(data, OFFSET_ATTRIBUTES);
178     }
179 
setFlags(int flags)180     private void setFlags(int flags) {
181         LittleEndian.setInt8(data, OFFSET_ATTRIBUTES, flags);
182     }
183 
isDirectory()184     public boolean isDirectory() {
185         return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_DIRECTORY);
186     }
187 
create(boolean directory)188     public static FatDirectoryEntry create(boolean directory) {
189         final FatDirectoryEntry result = new FatDirectoryEntry();
190 
191         if (directory) {
192             result.setFlags(F_DIRECTORY);
193         }
194 
195         /* initialize date and time fields */
196 
197         final long now = System.currentTimeMillis();
198         result.setCreated(now);
199         result.setLastAccessed(now);
200         result.setLastModified(now);
201 
202         return result;
203     }
204 
createVolumeLabel(String volumeLabel)205     public static FatDirectoryEntry createVolumeLabel(String volumeLabel) {
206         assert(volumeLabel != null);
207 
208         final byte[] data = new byte[SIZE];
209 
210         System.arraycopy(
211                     volumeLabel.getBytes(), 0,
212                     data, 0,
213                     volumeLabel.length());
214 
215         final FatDirectoryEntry result = new FatDirectoryEntry(data, false);
216         result.setFlags(FatDirectoryEntry.F_VOLUME_ID);
217         return result;
218     }
219 
getVolumeLabel()220     public String getVolumeLabel() {
221         if (!isVolumeLabel())
222             throw new UnsupportedOperationException("not a volume label");
223 
224         final StringBuilder sb = new StringBuilder();
225 
226         for (int i=0; i < AbstractDirectory.MAX_LABEL_LENGTH; i++) {
227             final byte b = this.data[i];
228 
229             if (b != 0) {
230                 sb.append((char) b);
231             } else {
232                 break;
233             }
234         }
235 
236         return sb.toString();
237     }
238 
getCreated()239     public long getCreated() {
240         return DosUtils.decodeDateTime(
241                 LittleEndian.getUInt16(data, 0x10),
242                 LittleEndian.getUInt16(data, 0x0e));
243     }
244 
setCreated(long created)245     public void setCreated(long created) {
246         LittleEndian.setInt16(data, 0x0e,
247                 DosUtils.encodeTime(created));
248         LittleEndian.setInt16(data, 0x10,
249                 DosUtils.encodeDate(created));
250 
251         this.dirty = true;
252     }
253 
getLastModified()254     public long getLastModified() {
255         return DosUtils.decodeDateTime(
256                 LittleEndian.getUInt16(data, 0x18),
257                 LittleEndian.getUInt16(data, 0x16));
258     }
259 
setLastModified(long lastModified)260     public void setLastModified(long lastModified) {
261         LittleEndian.setInt16(data, 0x16,
262                 DosUtils.encodeTime(lastModified));
263         LittleEndian.setInt16(data, 0x18,
264                 DosUtils.encodeDate(lastModified));
265 
266         this.dirty = true;
267     }
268 
getLastAccessed()269     public long getLastAccessed() {
270         return DosUtils.decodeDateTime(
271                 LittleEndian.getUInt16(data, 0x12),
272                 0); /* time is not recorded */
273     }
274 
setLastAccessed(long lastAccessed)275     public void setLastAccessed(long lastAccessed) {
276         LittleEndian.setInt16(data, 0x12,
277                 DosUtils.encodeDate(lastAccessed));
278 
279         this.dirty = true;
280     }
281 
282     /**
283      * Returns if this entry has been marked as deleted. A deleted entry has
284      * its first byte set to the magic {@link #ENTRY_DELETED_MAGIC} value.
285      *
286      * @return if this entry is marked as deleted
287      */
isDeleted()288     public boolean isDeleted() {
289         return  (LittleEndian.getUInt8(data, 0) == ENTRY_DELETED_MAGIC);
290     }
291 
292     /**
293      * Returns the size of this entry as stored at {@link #OFFSET_FILE_SIZE}.
294      *
295      * @return the size of the file represented by this entry
296      */
getLength()297     public long getLength() {
298         return LittleEndian.getUInt32(data, OFFSET_FILE_SIZE);
299     }
300 
301     /**
302      * Sets the size of this entry stored at {@link #OFFSET_FILE_SIZE}.
303      *
304      * @param length the new size of the file represented by this entry
305      * @throws IllegalArgumentException if {@code length} is out of range
306      */
setLength(long length)307     public void setLength(long length) throws IllegalArgumentException {
308         LittleEndian.setInt32(data, OFFSET_FILE_SIZE, length);
309     }
310 
311     /**
312      * Returns the {@code ShortName} that is stored in this directory entry or
313      * {@code null} if this entry has not been initialized.
314      *
315      * @return the {@code ShortName} stored in this entry or {@code null}
316      */
getShortName()317     public ShortName getShortName() {
318         if (this.data[0] == 0) {
319             return null;
320         } else {
321             return ShortName.parse(this.data);
322         }
323     }
324 
325     /**
326      * Does this entry refer to a file?
327      *
328      * @return
329      * @see org.jnode.fs.FSDirectoryEntry#isFile()
330      */
isFile()331     public boolean isFile() {
332         return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == 0);
333     }
334 
setShortName(ShortName sn)335     public void setShortName(ShortName sn) {
336         if (sn.equals(this.getShortName())) return;
337 
338         sn.write(this.data);
339         this.hasShortNameOnly = sn.hasShortNameOnly();
340         this.dirty = true;
341     }
342 
343     /**
344      * Returns the startCluster.
345      *
346      * @return int
347      */
getStartCluster()348     public long getStartCluster() {
349     	int lowBytes = LittleEndian.getUInt16(data, 0x1a);
350         long highBytes = LittleEndian.getUInt16(data, 0x14);
351         return ( highBytes << 16 | lowBytes );
352     }
353 
354     /**
355      * Sets the startCluster.
356      *
357      * @param startCluster The startCluster to set
358      */
setStartCluster(long startCluster)359     void setStartCluster(long startCluster) {
360         if ( startCluster > Integer.MAX_VALUE ) throw new AssertionError();
361 
362         LittleEndian.setInt16(data, 0x1a, (int) startCluster);
363         LittleEndian.setInt16(data, 0x14, (int)(startCluster >>> 16));
364     }
365 
366     @Override
toString()367     public String toString() {
368         return getClass().getSimpleName() +
369                 " [name=" + getShortName() + "]"; //NOI18N
370     }
371 
372     /**
373      * Writes this directory entry into the specified buffer.
374      *
375      * @param buff the buffer to write this entry to
376      */
write(ByteBuffer buff)377     void write(ByteBuffer buff) {
378         buff.put(data);
379         this.dirty = false;
380     }
381 
382     /**
383      * Returns if the read-only flag is set for this entry. Do not confuse
384      * this with {@link #isReadOnly()}.
385      *
386      * @return if the read only file system flag is set on this entry
387      * @see #F_READONLY
388      * @see #setReadonlyFlag(boolean)
389      */
isReadonlyFlag()390     public boolean isReadonlyFlag() {
391         return ((getFlags() & F_READONLY) != 0);
392     }
393 
394     /**
395      * Updates the read-only file system flag for this entry.
396      *
397      * @param isReadonly the new value for the read-only flag
398      * @see #F_READONLY
399      * @see #isReadonlyFlag()
400      */
setReadonlyFlag(boolean isReadonly)401     public void setReadonlyFlag(boolean isReadonly) {
402         setFlag(F_READONLY, isReadonly);
403     }
404 
getLfnPart()405     String getLfnPart() {
406         final char[] unicodechar = new char[13];
407 
408         unicodechar[0] = (char) LittleEndian.getUInt16(data, 1);
409         unicodechar[1] = (char) LittleEndian.getUInt16(data, 3);
410         unicodechar[2] = (char) LittleEndian.getUInt16(data, 5);
411         unicodechar[3] = (char) LittleEndian.getUInt16(data, 7);
412         unicodechar[4] = (char) LittleEndian.getUInt16(data, 9);
413         unicodechar[5] = (char) LittleEndian.getUInt16(data, 14);
414         unicodechar[6] = (char) LittleEndian.getUInt16(data, 16);
415         unicodechar[7] = (char) LittleEndian.getUInt16(data, 18);
416         unicodechar[8] = (char) LittleEndian.getUInt16(data, 20);
417         unicodechar[9] = (char) LittleEndian.getUInt16(data, 22);
418         unicodechar[10] = (char) LittleEndian.getUInt16(data, 24);
419         unicodechar[11] = (char) LittleEndian.getUInt16(data, 28);
420         unicodechar[12] = (char) LittleEndian.getUInt16(data, 30);
421 
422         int end = 0;
423 
424         while ((end < 13) && (unicodechar[end] != '\0')) {
425             end++;
426         }
427 
428         return new String(unicodechar).substring(0, end);
429     }
430 
431 }
432