aboutsummaryrefslogtreecommitdiff
path: root/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
blob: b8bb6888ca184251ae036ec4eabed6a3ba14d2f4 (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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
/*
 * Copyright 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.jimfs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.jimfs.Util.clear;
import static com.google.common.jimfs.Util.nextPowerOf2;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.UnsignedBytes;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * A mutable, resizable store for bytes. Bytes are stored in fixed-sized byte arrays (blocks)
 * allocated by a {@link HeapDisk}.
 *
 * @author Colin Decker
 */
final class RegularFile extends File {

  private final ReadWriteLock lock = new ReentrantReadWriteLock();

  private final HeapDisk disk;

  /** Block list for the file. */
  private byte[][] blocks;
  /** Block count for the the file, which also acts as the head of the block list. */
  private int blockCount;

  private long size;

  /** Creates a new regular file with the given ID and using the given disk. */
  public static RegularFile create(int id, HeapDisk disk) {
    return new RegularFile(id, disk, new byte[32][], 0, 0);
  }

  RegularFile(int id, HeapDisk disk, byte[][] blocks, int blockCount, long size) {
    super(id);
    this.disk = checkNotNull(disk);
    this.blocks = checkNotNull(blocks);
    this.blockCount = blockCount;

    checkArgument(size >= 0);
    this.size = size;
  }

  private int openCount = 0;
  private boolean deleted = false;

  /** Returns the read lock for this file. */
  public Lock readLock() {
    return lock.readLock();
  }

  /** Returns the write lock for this file. */
  public Lock writeLock() {
    return lock.writeLock();
  }

  // lower-level methods dealing with the blocks array

  private void expandIfNecessary(int minBlockCount) {
    if (minBlockCount > blocks.length) {
      this.blocks = Arrays.copyOf(blocks, nextPowerOf2(minBlockCount));
    }
  }

  /** Returns the number of blocks this file contains. */
  int blockCount() {
    return blockCount;
  }

  /** Copies the last {@code count} blocks from this file to the end of the given target file. */
  void copyBlocksTo(RegularFile target, int count) {
    int start = blockCount - count;
    int targetEnd = target.blockCount + count;
    target.expandIfNecessary(targetEnd);

    System.arraycopy(this.blocks, start, target.blocks, target.blockCount, count);
    target.blockCount = targetEnd;
  }

  /** Transfers the last {@code count} blocks from this file to the end of the given target file. */
  void transferBlocksTo(RegularFile target, int count) {
    copyBlocksTo(target, count);
    truncateBlocks(blockCount - count);
  }

  /** Truncates the blocks of this file to the given block count. */
  void truncateBlocks(int count) {
    clear(blocks, count, blockCount - count);
    blockCount = count;
  }

  /** Adds the given block to the end of this file. */
  void addBlock(byte[] block) {
    expandIfNecessary(blockCount + 1);
    blocks[blockCount++] = block;
  }

  /** Gets the block at the given index in this file. */
  @VisibleForTesting
  byte[] getBlock(int index) {
    return blocks[index];
  }

  // end of lower-level methods dealing with the blocks array

  /**
   * Gets the current size of this file in bytes. Does not do locking, so should only be called when
   * holding a lock.
   */
  public long sizeWithoutLocking() {
    return size;
  }

  // need to lock in these methods since they're defined by an interface

  @Override
  public long size() {
    readLock().lock();
    try {
      return size;
    } finally {
      readLock().unlock();
    }
  }

  @Override
  RegularFile copyWithoutContent(int id) {
    byte[][] copyBlocks = new byte[Math.max(blockCount * 2, 32)][];
    return new RegularFile(id, disk, copyBlocks, 0, size);
  }

  @Override
  void copyContentTo(File file) throws IOException {
    RegularFile copy = (RegularFile) file;
    disk.allocate(copy, blockCount);

    for (int i = 0; i < blockCount; i++) {
      byte[] block = blocks[i];
      byte[] copyBlock = copy.blocks[i];
      System.arraycopy(block, 0, copyBlock, 0, block.length);
    }
  }

  @Override
  ReadWriteLock contentLock() {
    return lock;
  }

  // opened/closed/delete don't use the read/write lock... they only need to ensure that they are
  // synchronized among themselves

  @Override
  public synchronized void opened() {
    openCount++;
  }

  @Override
  public synchronized void closed() {
    if (--openCount == 0 && deleted) {
      deleteContents();
    }
  }

  /**
   * Marks this file as deleted. If there are no streams or channels open to the file, its contents
   * are deleted if necessary.
   */
  @Override
  public synchronized void deleted() {
    if (links() == 0) {
      deleted = true;
      if (openCount == 0) {
        deleteContents();
      }
    }
  }

  /**
   * Deletes the contents of this file. Called when this file has been deleted and all open streams
   * and channels to it have been closed.
   */
  private void deleteContents() {
    disk.free(this);
    size = 0;
  }

  /**
   * Truncates this file to the given {@code size}. If the given size is less than the current size
   * of this file, the size of the file is reduced to the given size and any bytes beyond that size
   * are lost. If the given size is greater than the current size of the file, this method does
   * nothing. Returns {@code true} if this file was modified by the call (its size changed) and
   * {@code false} otherwise.
   */
  public boolean truncate(long size) {
    if (size >= this.size) {
      return false;
    }

    long lastPosition = size - 1;
    this.size = size;

    int newBlockCount = blockIndex(lastPosition) + 1;
    int blocksToRemove = blockCount - newBlockCount;
    if (blocksToRemove > 0) {
      disk.free(this, blocksToRemove);
    }

    return true;
  }

  /** Prepares for a write of len bytes starting at position pos. */
  private void prepareForWrite(long pos, long len) throws IOException {
    long end = pos + len;

    // allocate any additional blocks needed
    int lastBlockIndex = blockCount - 1;
    int endBlockIndex = blockIndex(end - 1);

    if (endBlockIndex > lastBlockIndex) {
      int additionalBlocksNeeded = endBlockIndex - lastBlockIndex;
      disk.allocate(this, additionalBlocksNeeded);
    }

    // zero bytes between current size and pos
    if (pos > size) {
      long remaining = pos - size;

      int blockIndex = blockIndex(size);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(size);

      remaining -= zero(block, off, length(off, remaining));

      while (remaining > 0) {
        block = blocks[++blockIndex];

        remaining -= zero(block, 0, length(remaining));
      }

      size = pos;
    }
  }

  /**
   * Writes the given byte to this file at position {@code pos}. {@code pos} may be greater than the
   * current size of this file, in which case this file is resized and all bytes between the current
   * size and {@code pos} are set to 0. Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  public int write(long pos, byte b) throws IOException {
    prepareForWrite(pos, 1);

    byte[] block = blocks[blockIndex(pos)];
    int off = offsetInBlock(pos);
    block[off] = b;

    if (pos >= size) {
      size = pos + 1;
    }

    return 1;
  }

  /**
   * Writes {@code len} bytes starting at offset {@code off} in the given byte array to this file
   * starting at position {@code pos}. {@code pos} may be greater than the current size of this
   * file, in which case this file is resized and all bytes between the current size and {@code pos}
   * are set to 0. Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  public int write(long pos, byte[] b, int off, int len) throws IOException {
    prepareForWrite(pos, len);

    if (len == 0) {
      return 0;
    }

    int remaining = len;

    int blockIndex = blockIndex(pos);
    byte[] block = blocks[blockIndex];
    int offInBlock = offsetInBlock(pos);

    int written = put(block, offInBlock, b, off, length(offInBlock, remaining));
    remaining -= written;
    off += written;

    while (remaining > 0) {
      block = blocks[++blockIndex];

      written = put(block, 0, b, off, length(remaining));
      remaining -= written;
      off += written;
    }

    long endPos = pos + len;
    if (endPos > size) {
      size = endPos;
    }

    return len;
  }

  /**
   * Writes all available bytes from buffer {@code buf} to this file starting at position {@code
   * pos}. {@code pos} may be greater than the current size of this file, in which case this file is
   * resized and all bytes between the current size and {@code pos} are set to 0. Returns the number
   * of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  public int write(long pos, ByteBuffer buf) throws IOException {
    int len = buf.remaining();

    prepareForWrite(pos, len);

    if (len == 0) {
      return 0;
    }

    int blockIndex = blockIndex(pos);
    byte[] block = blocks[blockIndex];
    int off = offsetInBlock(pos);

    put(block, off, buf);

    while (buf.hasRemaining()) {
      block = blocks[++blockIndex];

      put(block, 0, buf);
    }

    long endPos = pos + len;
    if (endPos > size) {
      size = endPos;
    }

    return len;
  }

  /**
   * Writes all available bytes from each buffer in {@code bufs}, in order, to this file starting at
   * position {@code pos}. {@code pos} may be greater than the current size of this file, in which
   * case this file is resized and all bytes between the current size and {@code pos} are set to 0.
   * Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  public long write(long pos, Iterable<ByteBuffer> bufs) throws IOException {
    long start = pos;
    for (ByteBuffer buf : bufs) {
      pos += write(pos, buf);
    }
    return pos - start;
  }

  /**
   * Transfers up to {@code count} bytes from the given channel to this file starting at position
   * {@code pos}. Returns the number of bytes transferred. If {@code pos} is greater than the
   * current size of this file, the file is truncated up to size {@code pos} before writing.
   *
   * @throws IOException if the file needs more blocks but the disk is full or if reading from src
   *     throws an exception
   */
  public long transferFrom(ReadableByteChannel src, long pos, long count) throws IOException {
    prepareForWrite(pos, 0); // don't assume the full count bytes will be written

    if (count == 0) {
      return 0;
    }

    long remaining = count;

    int blockIndex = blockIndex(pos);
    byte[] block = blockForWrite(blockIndex);
    int off = offsetInBlock(pos);

    ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));

    long currentPos = pos;
    int read = 0;
    while (buf.hasRemaining()) {
      read = src.read(buf);
      if (read == -1) {
        break;
      }

      currentPos += read;
      remaining -= read;
    }

    // update size before trying to get next block in case the disk is out of space
    if (currentPos > size) {
      size = currentPos;
    }

    if (read != -1) {
      outer:
      while (remaining > 0) {
        block = blockForWrite(++blockIndex);

        buf = ByteBuffer.wrap(block, 0, length(remaining));
        while (buf.hasRemaining()) {
          read = src.read(buf);
          if (read == -1) {
            break outer;
          }

          currentPos += read;
          remaining -= read;
        }

        if (currentPos > size) {
          size = currentPos;
        }
      }
    }

    if (currentPos > size) {
      size = currentPos;
    }

    return currentPos - pos;
  }

  /**
   * Reads the byte at position {@code pos} in this file as an unsigned integer in the range 0-255.
   * If {@code pos} is greater than or equal to the size of this file, returns -1 instead.
   */
  public int read(long pos) {
    if (pos >= size) {
      return -1;
    }

    byte[] block = blocks[blockIndex(pos)];
    int off = offsetInBlock(pos);
    return UnsignedBytes.toInt(block[off]);
  }

  /**
   * Reads up to {@code len} bytes starting at position {@code pos} in this file to the given byte
   * array starting at offset {@code off}. Returns the number of bytes actually read or -1 if {@code
   * pos} is greater than or equal to the size of this file.
   */
  public int read(long pos, byte[] b, int off, int len) {
    // since max is len (an int), result is guaranteed to be an int
    int bytesToRead = (int) bytesToRead(pos, len);

    if (bytesToRead > 0) {
      int remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int offsetInBlock = offsetInBlock(pos);

      int read = get(block, offsetInBlock, b, off, length(offsetInBlock, remaining));
      remaining -= read;
      off += read;

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];

        read = get(block, 0, b, off, length(remaining));
        remaining -= read;
        off += read;
      }
    }

    return bytesToRead;
  }

  /**
   * Reads up to {@code buf.remaining()} bytes starting at position {@code pos} in this file to the
   * given buffer. Returns the number of bytes read or -1 if {@code pos} is greater than or equal to
   * the size of this file.
   */
  public int read(long pos, ByteBuffer buf) {
    // since max is buf.remaining() (an int), result is guaranteed to be an int
    int bytesToRead = (int) bytesToRead(pos, buf.remaining());

    if (bytesToRead > 0) {
      int remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(pos);

      remaining -= get(block, off, buf, length(off, remaining));

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];
        remaining -= get(block, 0, buf, length(remaining));
      }
    }

    return bytesToRead;
  }

  /**
   * Reads up to the total {@code remaining()} number of bytes in each of {@code bufs} starting at
   * position {@code pos} in this file to the given buffers, in order. Returns the number of bytes
   * read or -1 if {@code pos} is greater than or equal to the size of this file.
   */
  public long read(long pos, Iterable<ByteBuffer> bufs) {
    if (pos >= size()) {
      return -1;
    }

    long start = pos;
    for (ByteBuffer buf : bufs) {
      int read = read(pos, buf);
      if (read == -1) {
        break;
      } else {
        pos += read;
      }
    }

    return pos - start;
  }

  /**
   * Transfers up to {@code count} bytes to the given channel starting at position {@code pos} in
   * this file. Returns the number of bytes transferred, possibly 0. Note that unlike all other read
   * methods in this class, this method does not return -1 if {@code pos} is greater than or equal
   * to the current size. This for consistency with {@link FileChannel#transferTo}, which this
   * method is primarily intended as an implementation of.
   */
  public long transferTo(long pos, long count, WritableByteChannel dest) throws IOException {
    long bytesToRead = bytesToRead(pos, count);

    if (bytesToRead > 0) {
      long remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(pos);

      ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
      while (buf.hasRemaining()) {
        remaining -= dest.write(buf);
      }
      buf.clear();

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];

        buf = ByteBuffer.wrap(block, 0, length(remaining));
        while (buf.hasRemaining()) {
          remaining -= dest.write(buf);
        }
        buf.clear();
      }
    }

    return Math.max(bytesToRead, 0); // don't return -1 for this method
  }

  /** Gets the block at the given index, expanding to create the block if necessary. */
  private byte[] blockForWrite(int index) throws IOException {
    if (index >= blockCount) {
      int additionalBlocksNeeded = index - blockCount + 1;
      disk.allocate(this, additionalBlocksNeeded);
    }

    return blocks[index];
  }

  private int blockIndex(long position) {
    return (int) (position / disk.blockSize());
  }

  private int offsetInBlock(long position) {
    return (int) (position % disk.blockSize());
  }

  private int length(long max) {
    return (int) Math.min(disk.blockSize(), max);
  }

  private int length(int off, long max) {
    return (int) Math.min(disk.blockSize() - off, max);
  }

  /**
   * Returns the number of bytes that can be read starting at position {@code pos} (up to a maximum
   * of {@code max}) or -1 if {@code pos} is greater than or equal to the current size.
   */
  private long bytesToRead(long pos, long max) {
    long available = size - pos;
    if (available <= 0) {
      return -1;
    }
    return Math.min(available, max);
  }

  /** Zeroes len bytes in the given block starting at the given offset. Returns len. */
  private static int zero(byte[] block, int offset, int len) {
    Util.zero(block, offset, len);
    return len;
  }

  /** Puts the given slice of the given array at the given offset in the given block. */
  private static int put(byte[] block, int offset, byte[] b, int off, int len) {
    System.arraycopy(b, off, block, offset, len);
    return len;
  }

  /** Puts the contents of the given byte buffer at the given offset in the given block. */
  private static int put(byte[] block, int offset, ByteBuffer buf) {
    int len = Math.min(block.length - offset, buf.remaining());
    buf.get(block, offset, len);
    return len;
  }

  /**
   * Reads len bytes starting at the given offset in the given block into the given slice of the
   * given byte array.
   */
  private static int get(byte[] block, int offset, byte[] b, int off, int len) {
    System.arraycopy(block, offset, b, off, len);
    return len;
  }

  /** Reads len bytes starting at the given offset in the given block into the given byte buffer. */
  private static int get(byte[] block, int offset, ByteBuffer buf, int len) {
    buf.put(block, offset, len);
    return len;
  }
}