summaryrefslogtreecommitdiff
path: root/src/main/java/de/waldheinz/fs/fat/FatDirectoryEntry.java
blob: b7a361152250a34c518a54b9f186a4f6882db355 (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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/*
 * 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 de.waldheinz.fs.AbstractFsObject;
import java.nio.ByteBuffer;

/**
 * 
 *
 * @author Ewout Prangsma &lt;epr at jnode.org&gt;
 * @author Matthias Treydte &lt;waldheinz at gmail.com&gt;
 */
final class FatDirectoryEntry extends AbstractFsObject {
    
    /**
     * The size in bytes of an FAT directory entry.
     */
    public final static int SIZE = 32;
    
    /**
     * The offset to the attributes byte.
     */
    private static final int OFFSET_ATTRIBUTES = 0x0b;
    
    /**
     * The offset to the file size dword.
     */
    private static final int OFFSET_FILE_SIZE = 0x1c;
    
    private static final int F_READONLY = 0x01;
    private static final int F_HIDDEN = 0x02;
    private static final int F_SYSTEM = 0x04;
    private static final int F_VOLUME_ID = 0x08;
    private static final int F_DIRECTORY = 0x10;
    private static final int F_ARCHIVE = 0x20;

    private static final int MAX_CLUSTER = 0xFFFF;
    
    /**
     * The magic byte denoting that this entry was deleted and is free
     * for reuse.
     *
     * @see #isDeleted() 
     */
    public static final int ENTRY_DELETED_MAGIC = 0xe5;
    
    private final byte[] data;
    private boolean dirty;
    boolean hasShortNameOnly;
    
    FatDirectoryEntry(byte[] data, boolean readOnly) {
        super(readOnly);
        
        this.data = data;
    }
    
    private FatDirectoryEntry() {
        this(new byte[SIZE], false);
        
    }
    
    /**
     * Reads a {@code FatDirectoryEntry} from the specified {@code ByteBuffer}.
     * The buffer must have at least {@link #SIZE} bytes remaining. The entry
     * is read from the buffer's current position, and if this method returns
     * non-null the position will have advanced by {@link #SIZE} bytes,
     * otherwise the position will remain unchanged.
     *
     * @param buff the buffer to read the entry from
     * @param readOnly if the resulting {@code FatDirecoryEntry} should be
     *      read-only
     * @return the directory entry that was read from the buffer or {@code null}
     *      if there was no entry to read from the specified position (first
     *      byte was 0)
     */
    public static FatDirectoryEntry read(ByteBuffer buff, boolean readOnly) {
        assert (buff.remaining() >= SIZE);

        /* peek into the buffer to see if we're done with reading */
        
        if (buff.get(buff.position()) == 0) return null;

        /* read the directory entry */

        final byte[] data = new byte[SIZE];
        buff.get(data);
        return new FatDirectoryEntry(data, readOnly);
    }

    public static void writeNullEntry(ByteBuffer buff) {
        for (int i=0; i < SIZE; i++) {
            buff.put((byte) 0);
        }
    }
    
    /**
     * Decides if this entry is a "volume label" entry according to the FAT
     * specification.
     *
     * @return if this is a volume label entry
     */
    public boolean isVolumeLabel() {
        if (isLfnEntry()) return false;
        else return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_VOLUME_ID);
    }

    private void setFlag(int mask, boolean set) {
        final int oldFlags = getFlags();

        if (((oldFlags & mask) != 0) == set) return;
        
        if (set) {
            setFlags(oldFlags | mask);
        } else {
            setFlags(oldFlags & ~mask);
        }

        this.dirty = true;
    }

    public boolean isSystemFlag() {
        return ((getFlags() & F_SYSTEM) != 0);
    }

    public void setSystemFlag(boolean isSystem) {
        setFlag(F_SYSTEM, isSystem);
    }

    public boolean isArchiveFlag() {
        return ((getFlags() & F_ARCHIVE) != 0);
    }

    public void setArchiveFlag(boolean isArchive) {
        setFlag(F_ARCHIVE, isArchive);
    }
    
    public boolean isHiddenFlag() {
        return ((getFlags() & F_HIDDEN) != 0);
    }

    public void setHiddenFlag(boolean isHidden) {
        setFlag(F_HIDDEN, isHidden);
    }
    
    public boolean isVolumeIdFlag() {
        return ((getFlags() & F_VOLUME_ID) != 0);
    }
    
    public boolean isLfnEntry() {
        return isReadonlyFlag() && isSystemFlag() &&
                isHiddenFlag() && isVolumeIdFlag();
    }
    
    public boolean isDirty() {
        return dirty;
    }
    
    private int getFlags() {
        return LittleEndian.getUInt8(data, OFFSET_ATTRIBUTES);
    }
    
    private void setFlags(int flags) {
        LittleEndian.setInt8(data, OFFSET_ATTRIBUTES, flags);
    }
    
    public boolean isDirectory() {
        return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_DIRECTORY);
    }
    
    public static FatDirectoryEntry create(boolean directory) {
        final FatDirectoryEntry result = new FatDirectoryEntry();

        if (directory) {
            result.setFlags(F_DIRECTORY);
        }
        
        /* initialize date and time fields */

        final long now = System.currentTimeMillis();
        result.setCreated(now);
        result.setLastAccessed(now);
        result.setLastModified(now);
        
        return result;
    }
    
    public static FatDirectoryEntry createVolumeLabel(String volumeLabel) {
        assert(volumeLabel != null);
        
        final byte[] data = new byte[SIZE];
        
        System.arraycopy(
                    volumeLabel.getBytes(), 0,
                    data, 0,
                    volumeLabel.length());

        final FatDirectoryEntry result = new FatDirectoryEntry(data, false);
        result.setFlags(FatDirectoryEntry.F_VOLUME_ID);
        return result;
    }
    
    public String getVolumeLabel() {
        if (!isVolumeLabel())
            throw new UnsupportedOperationException("not a volume label");
            
        final StringBuilder sb = new StringBuilder();
        
        for (int i=0; i < AbstractDirectory.MAX_LABEL_LENGTH; i++) {
            final byte b = this.data[i];
            
            if (b != 0) {
                sb.append((char) b);
            } else {
                break;
            }
        }
        
        return sb.toString();
    }

    public long getCreated() {
        return DosUtils.decodeDateTime(
                LittleEndian.getUInt16(data, 0x10),
                LittleEndian.getUInt16(data, 0x0e));
    }
    
    public void setCreated(long created) {
        LittleEndian.setInt16(data, 0x0e,
                DosUtils.encodeTime(created));
        LittleEndian.setInt16(data, 0x10,
                DosUtils.encodeDate(created));

        this.dirty = true;
    }

    public long getLastModified() {
        return DosUtils.decodeDateTime(
                LittleEndian.getUInt16(data, 0x18),
                LittleEndian.getUInt16(data, 0x16));
    }

    public void setLastModified(long lastModified) {
        LittleEndian.setInt16(data, 0x16,
                DosUtils.encodeTime(lastModified));
        LittleEndian.setInt16(data, 0x18,
                DosUtils.encodeDate(lastModified));

        this.dirty = true;
    }

    public long getLastAccessed() {
        return DosUtils.decodeDateTime(
                LittleEndian.getUInt16(data, 0x12),
                0); /* time is not recorded */
    }
    
    public void setLastAccessed(long lastAccessed) {
        LittleEndian.setInt16(data, 0x12,
                DosUtils.encodeDate(lastAccessed));

        this.dirty = true;
    }
    
    /**
     * Returns if this entry has been marked as deleted. A deleted entry has
     * its first byte set to the magic {@link #ENTRY_DELETED_MAGIC} value.
     * 
     * @return if this entry is marked as deleted
     */
    public boolean isDeleted() {
        return  (LittleEndian.getUInt8(data, 0) == ENTRY_DELETED_MAGIC);
    }
    
    /**
     * Returns the size of this entry as stored at {@link #OFFSET_FILE_SIZE}.
     * 
     * @return the size of the file represented by this entry
     */
    public long getLength() {
        return LittleEndian.getUInt32(data, OFFSET_FILE_SIZE);
    }

    /**
     * Sets the size of this entry stored at {@link #OFFSET_FILE_SIZE}.
     * 
     * @param length the new size of the file represented by this entry
     * @throws IllegalArgumentException if {@code length} is out of range
     */
    public void setLength(long length) throws IllegalArgumentException {
        LittleEndian.setInt32(data, OFFSET_FILE_SIZE, length);
    }
    
    /**
     * Returns the {@code ShortName} that is stored in this directory entry or
     * {@code null} if this entry has not been initialized.
     * 
     * @return the {@code ShortName} stored in this entry or {@code null}
     */
    public ShortName getShortName() {
        if (this.data[0] == 0) {
            return null;
        } else {
            return ShortName.parse(this.data);
        }
    }
    
    /**
     * Does this entry refer to a file?
     *
     * @return
     * @see org.jnode.fs.FSDirectoryEntry#isFile()
     */
    public boolean isFile() {
        return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == 0);
    }
    
    public void setShortName(ShortName sn) {
        if (sn.equals(this.getShortName())) return;
        
        sn.write(this.data);
        this.hasShortNameOnly = sn.hasShortNameOnly();
        this.dirty = true;
    }

    /**
     * Returns the startCluster.
     * 
     * @return int
     */
    public long getStartCluster() {
    	int lowBytes = LittleEndian.getUInt16(data, 0x1a);
        long highBytes = LittleEndian.getUInt16(data, 0x14);
        return ( highBytes << 16 | lowBytes );
    }
    
    /**
     * Sets the startCluster.
     *
     * @param startCluster The startCluster to set
     */
    void setStartCluster(long startCluster) {
        if ( startCluster > Integer.MAX_VALUE ) throw new AssertionError();

        LittleEndian.setInt16(data, 0x1a, (int) startCluster);
        LittleEndian.setInt16(data, 0x14, (int)(startCluster >>> 16));
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() +
                " [name=" + getShortName() + "]"; //NOI18N
    }

    /**
     * Writes this directory entry into the specified buffer.
     *
     * @param buff the buffer to write this entry to
     */
    void write(ByteBuffer buff) {
        buff.put(data);
        this.dirty = false;
    }

    /**
     * Returns if the read-only flag is set for this entry. Do not confuse
     * this with {@link #isReadOnly()}.
     *
     * @return if the read only file system flag is set on this entry
     * @see #F_READONLY
     * @see #setReadonlyFlag(boolean) 
     */
    public boolean isReadonlyFlag() {
        return ((getFlags() & F_READONLY) != 0);
    }

    /**
     * Updates the read-only file system flag for this entry.
     *
     * @param isReadonly the new value for the read-only flag
     * @see #F_READONLY
     * @see #isReadonlyFlag() 
     */
    public void setReadonlyFlag(boolean isReadonly) {
        setFlag(F_READONLY, isReadonly);
    }
    
    String getLfnPart() {
        final char[] unicodechar = new char[13];

        unicodechar[0] = (char) LittleEndian.getUInt16(data, 1);
        unicodechar[1] = (char) LittleEndian.getUInt16(data, 3);
        unicodechar[2] = (char) LittleEndian.getUInt16(data, 5);
        unicodechar[3] = (char) LittleEndian.getUInt16(data, 7);
        unicodechar[4] = (char) LittleEndian.getUInt16(data, 9);
        unicodechar[5] = (char) LittleEndian.getUInt16(data, 14);
        unicodechar[6] = (char) LittleEndian.getUInt16(data, 16);
        unicodechar[7] = (char) LittleEndian.getUInt16(data, 18);
        unicodechar[8] = (char) LittleEndian.getUInt16(data, 20);
        unicodechar[9] = (char) LittleEndian.getUInt16(data, 22);
        unicodechar[10] = (char) LittleEndian.getUInt16(data, 24);
        unicodechar[11] = (char) LittleEndian.getUInt16(data, 28);
        unicodechar[12] = (char) LittleEndian.getUInt16(data, 30);

        int end = 0;

        while ((end < 13) && (unicodechar[end] != '\0')) {
            end++;
        }
        
        return new String(unicodechar).substring(0, end);
    }

}