/*- * Copyright (C) 2006-2009 Erik Larsson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catacombae.hfs; import java.util.LinkedList; import org.catacombae.util.ObjectContainer; import org.catacombae.hfs.types.hfscommon.CommonHFSExtentDescriptor; import org.catacombae.hfs.types.hfscommon.CommonHFSVolumeHeader; import org.catacombae.io.ReadableRandomAccessStream; /** * @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a> */ public abstract class AllocationFile { protected final HFSVolume parentView; protected final ReadableRandomAccessStream allocationFileStream; protected AllocationFile(HFSVolume parentView, ReadableRandomAccessStream allocationFileStream) { this.parentView = parentView; this.allocationFileStream = allocationFileStream; if(this.parentView == null) throw new IllegalArgumentException("parentView == null"); } /** * Tells if the allocation block addressed by <code>blockNumber</code> is * used or not. * * @param blockNumber the block number to probe for allocation status. * @return whether the block <code>blockNumber</code> is used (true) or not * (false). * @throws java.lang.IllegalArgumentException if <code>blockNumber</code> * is out of range. */ public synchronized boolean isAllocationBlockUsed(long blockNumber) throws IllegalArgumentException { CommonHFSVolumeHeader vh = parentView.getVolumeHeader(); return isAllocationBlockUsed(blockNumber, vh); } private synchronized boolean isAllocationBlockUsed(long blockNumber, CommonHFSVolumeHeader vh) { long numAllocationBlocks = vh.getTotalBlocks(); if(blockNumber >= numAllocationBlocks) throw new IllegalArgumentException("Block number (" + blockNumber + ") is beyond the highest block of the volume (" + (numAllocationBlocks-1) + ")."); long byteIndex = blockNumber / 8; allocationFileStream.seek(byteIndex); int currentByte = allocationFileStream.read(); if(currentByte >= 0) return (currentByte & (1 << 7 - (blockNumber % 8))) != 0; else throw new RuntimeException("No data left in stream! allocationFileStream.getFilePointer()=" + allocationFileStream.getFilePointer() + " allocationFileStream.length()=" + allocationFileStream.length()); } /** * Loops through the entire allocation file and counts the number of blocks * that are marked as free. * * @return the number of free blocks in the allocation file. */ /* public synchronized long countFreeBlocks() { return countBlocks(false); } */ /** * Loops through the entire allocation file and counts the number of blocks * that are marked as allocated. * * @return the number of allocated blocks in the allocation file. */ /* public synchronized long countAllocatedBlocks() { return countBlocks(true); } */ /** * Loops through the entire allocation file to count the number of used and free blocks on the * volume. The output is placed in two <code>ObjectContainer</code>s * * @param oFreeBlocks (optional) variable where the algorithm stores the free block count. * @param oUsedBlocks (optional) variable where the algorithm stores the used block count. * @param stop (optional) variable which can be set to abort the block counting process. Must * initally be set to <code>false</code> or no work will be done whatsoever. * @return the total number of allocation blocks on the volume. */ public long countBlocks(ObjectContainer<Long> oFreeBlocks, ObjectContainer<Long> oUsedBlocks, ObjectContainer<Boolean> stop) { CommonHFSVolumeHeader vh = parentView.getVolumeHeader(); byte[] currentBlock = new byte[128*1024]; final long totalBlocks = vh.getTotalBlocks(); long blockCount = 0; //long allocatedBlockCount = 0; long usedBlockCount = 0; //int blockValue = (usedBlocks?0x1:0x0); if(stop == null) stop = new ObjectContainer<Boolean>(false); //System.err.println("countBlocks(): totalBlocks=" + totalBlocks); //System.err.println("countBlocks(): allocationFileStream.length()=" + allocationFileStream.length()); allocationFileStream.seek(0); while(blockCount < totalBlocks && !stop.o) { //System.err.println("countBlocks(): blockCount=" + blockCount); //System.err.println("countBlocks(): allocationFileStream.getFilePointer()=" + allocationFileStream.getFilePointer()); //System.err.println("countBlocks(): =" + ); //System.out.println("countBlocks(): Reading a blob (" + currentBlock.length + " bytes)..."); int bytesRead = allocationFileStream.read(currentBlock); //System.out.println("countBlocks(): ..." + bytesRead + " bytes read."); if(bytesRead >= 0) { for(int i = 0; i < bytesRead && blockCount < totalBlocks && !stop.o; ++i) { byte currentByte = currentBlock[i]; for(int j = 0; j < 8 && blockCount < totalBlocks && !stop.o; ++j) { ++blockCount; if(((currentByte >> (7 - j)) & 0x1) == 0x1) ++usedBlockCount; } } } else throw new RuntimeException("Could not read all blocks from allocation file!"); } if(blockCount != totalBlocks) throw new RuntimeException("[INTERNAL ERROR] blockCount(" + blockCount + ") != totalBlocks(" + totalBlocks + ")"); if(oFreeBlocks != null) oFreeBlocks.o = blockCount-usedBlockCount; if(oUsedBlocks != null) oUsedBlocks.o = usedBlockCount; return totalBlocks; } /** * Creates an implementation specific extent descriptor from the given data. * * @param startBlock the first block number of the extent. * @param blockCount the number of blocks that the extent ranges over. * @return an implementation specific representation of an extent descriptor * created from the <code>startBlock</code> and <code>allocatedBlockCount</code> * parameters. * @throws java.lang.IllegalArgumentException if any of the values of <code> * startBlock</code> or <code>allocatedBlockCount</code> are out of range for the * implementation (16-bit signed integer value for HFS, 32-bit signed * integer value for HFS+). */ protected abstract CommonHFSExtentDescriptor createExtentDescriptor(long startBlock, long blockCount) throws IllegalArgumentException; /** * Calculates an array of extents that are currently free, and that matches * the supplied file size. Every effort will be made to find the most * perfectly fitting match, i.e. with as few extents as possible.<br> * If no match is found (the volume does not have enough free blocks to hold * the specified size), <code>null</code> is returned. * * @param fileSize the size of the data region to be allocated, in bytes. * @return an array of descriptors of the extents where the data region * can be stored on disk. */ public synchronized CommonHFSExtentDescriptor[] findFreeSpace(final long fileSize) { if(fileSize < 0) throw new IllegalArgumentException("Negative file size: " + fileSize); CommonHFSVolumeHeader vh = parentView.getVolumeHeader(); final long blockSize = vh.getAllocationBlockSize(); final long totalBlocks = vh.getTotalBlocks(); final long blocksToAllocate = fileSize / blockSize + (fileSize % blockSize != 0 ? 1 : 0); long blocksLeft = blocksToAllocate; /* * Search for the closest matching region, i.e. the smallest region that * is larger than or equal to fileSize, or if none can be found, the * largest region that is smaller than fileSize. */ // Loop through the entire allocation file ByteRegion closestMatchAbove = new ByteRegion(); ByteRegion closestMatchBelow = new ByteRegion(); LinkedList<ByteRegion> allocations = new LinkedList<ByteRegion>(); while(blocksLeft > 0) { if(closestMatchAbove == null) closestMatchAbove = new ByteRegion(); if(closestMatchBelow == null) closestMatchBelow = new ByteRegion(); closestMatchAbove.reset(); closestMatchBelow.reset(); long regionStart = -1; for(int i = 0; i < totalBlocks; ++i) { // Check that we do not collide with any already allocated regions int j = 0; for(ByteRegion br : allocations) { if(i >= br.offset && i < (br.offset+br.length)) break; ++j; } if(j != allocations.size()) continue; if(!isAllocationBlockUsed(i, vh)) { if(regionStart == -1) regionStart = i; } else { if(regionStart != -1) { long length = i-regionStart; if(length > blocksLeft) { if(closestMatchAbove.length < 0 || closestMatchAbove.length > length) { closestMatchAbove.offset = regionStart; closestMatchAbove.length = length; } } else if(length < blocksLeft) { if(closestMatchBelow.length < 0 || closestMatchBelow.length < length) { closestMatchBelow.offset = regionStart; closestMatchBelow.length = length; } } else { closestMatchAbove.offset = regionStart; closestMatchAbove.length = length; break; // A perfect match, so we don't need to search more. } } regionStart = -1; } } if(closestMatchAbove.isValid()) { // We found a fitting region for the rest of the data //result.add(createExtentDescriptor(closestMatchAbove.offset, // blocksLeft)); closestMatchAbove.length = blocksLeft; allocations.add(closestMatchAbove); blocksLeft = 0; closestMatchAbove = null; // It is taken } else if(closestMatchBelow.isValid()) { //result.add(createExtentDescriptor(closestMatchBelow.offset, // closestMatchBelow.length)); allocations.add(closestMatchBelow); blocksLeft -= closestMatchBelow.length; closestMatchBelow = null; // It is taken } else { // We're out of free blocks... return null; } } if(blocksLeft != 0) throw new RuntimeException("[INTERNAL ERROR] blocksLeft(" + blocksLeft + ") != 0 [closestMatchAbove.offset=" + closestMatchAbove.offset + ",closestMatchAbove.length=" + closestMatchAbove.length + "]"); CommonHFSExtentDescriptor[] result = new CommonHFSExtentDescriptor[allocations.size()]; int i = 0; for(ByteRegion br : allocations) result[i++] = createExtentDescriptor(br.offset, br.length); return result; } public void close() { allocationFileStream.close(); } private class ByteRegion { public long offset; public long length; public void reset() { offset = -1; length = -1; } public boolean isValid() { return offset > 0 && length > 0; } } }