summaryrefslogtreecommitdiffstats
path: root/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java
blob: a6b3b73eba2f93ede21600f15d7042ea55886a3b (plain) (blame)
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
package org.openslx.virtualization.disk;

import java.io.RandomAccessFile;

import org.openslx.virtualization.Version;

/**
 * QCOW2 disk image for virtual machines.
 * 
 * A QCOW2 disk image consists of a header, one L1 table and several L2 tables used for lookup data
 * clusters in the file via a two-level lookup. The QCOW2 header contains the following fields:
 * 
 * <pre>
 *   QCOW2 (version 2 and 3) header format:
 *   
 *   magic                   (4 byte)
 *   version                 (4 byte)
 *   backing_file_offset     (8 byte)
 *   backing_file_size       (4 byte)
 *   cluster_bits            (4 byte)
 *   size                    (8 byte)
 *   crypt_method            (4 byte)
 *   l1_size                 (4 byte)
 *   l1_table_offset         (8 byte)
 *   refcount_table_offset   (8 byte)
 *   refcount_table_clusters (4 byte)
 *   nb_snapshots            (4 byte)
 *   snapshots_offset        (8 byte)
 *   incompatible_features   (8 byte)  [*]
 *   compatible_features     (8 byte)  [*]
 *   autoclear_features      (8 byte)  [*]
 *   refcount_order          (8 byte)  [*]
 *   header_length           (4 byte)  [*]
 *
 *   [*] these fields are only available in the QCOW2 version 3 header format
 * </pre>
 * 
 * @author Manuel Bentele
 * @version 1.0
 */
public class DiskImageQcow2 extends DiskImage
{
	/**
	 * Big endian representation of the big endian QCOW2 magic bytes <code>QFI\xFB</code>.
	 */
	private static final int QCOW2_MAGIC = 0x514649fb;

	/**
	 * Creates a new QCOW2 disk image from an existing QCOW2 image file.
	 * 
	 * @param diskImage file to a QCOW2 disk storing the image content.
	 */
	DiskImageQcow2( RandomAccessFile diskImage )
	{
		super( diskImage );
	}

	/**
	 * Probe specified file with unknown format to be a QCOW2 disk image file.
	 * 
	 * @param diskImage file with unknown format that should be probed.
	 * @return state whether file is a QCOW2 disk image or not.
	 * 
	 * @throws DiskImageException cannot probe specified file with unknown format.
	 */
	public static boolean probe( RandomAccessFile diskImage ) throws DiskImageException
	{
		final boolean isQcow2ImageFormat;

		// goto the beginning of the disk image to read the disk image
		final int diskImageMagic = DiskImageUtils.readInt( diskImage, 0 );

		// check if disk image's magic bytes can be found
		if ( diskImageMagic == DiskImageQcow2.QCOW2_MAGIC ) {
			isQcow2ImageFormat = true;
		} else {
			isQcow2ImageFormat = false;
		}

		return isQcow2ImageFormat;
	}

	@Override
	public boolean isStandalone() throws DiskImageException
	{
		final RandomAccessFile diskFile = this.getDiskImage();
		final long qcowBackingFileOffset = DiskImageUtils.readLong( diskFile, 8 );
		final boolean qcowStandalone;

		// check if QCOW2 image does not refer to any backing file
		if ( qcowBackingFileOffset == 0 ) {
			qcowStandalone = true;
		} else {
			qcowStandalone = false;
		}

		return qcowStandalone;
	}

	@Override
	public boolean isCompressed() throws DiskImageException
	{
		final RandomAccessFile diskFile = this.getDiskImage();
		final boolean qcowUseExtendedL2;
		boolean qcowCompressed = false;

		// check if QCOW2 image uses extended L2 tables
		// extended L2 tables are only possible in QCOW2 version 3 header format
		if ( this.getVersion().getMajor() >= Short.valueOf( "3" ) ) {
			// read incompatible feature bits
			final long qcowIncompatibleFeatures = DiskImageUtils.readLong( diskFile, 72 );

			// support for extended L2 tables is enabled if bit 4 is set
			qcowUseExtendedL2 = ( ( ( qcowIncompatibleFeatures & 0x000000000010 ) >>> 4 ) == 1 );
		} else {
			qcowUseExtendedL2 = false;
		}

		// get number of entries in L1 table
		final int qcowL1TableSize = DiskImageUtils.readInt( diskFile, 36 );

		// check if a valid L1 table is present
		if ( qcowL1TableSize > 0 ) {
			// QCOW2 image contains active L1 table with more than 0 entries: l1_size > 0
			// search for first L2 table and its first entry to get compression bit

			// get cluster bits to calculate the cluster size
			final int qcowClusterBits = DiskImageUtils.readInt( diskFile, 20 );
			final int qcowClusterSize = ( 1 << qcowClusterBits );

			// entries of a L1 table have always the size of 8 byte (64 bit)
			final int qcowL1TableEntrySize = 8;

			// entries of a L2 table have either the size of 8 or 16 byte (64 or 128 bit)
			final int qcowL2TableEntrySize = ( qcowUseExtendedL2 ) ? 16 : 8;

			// calculate number of L2 table entries
			final int qcowL2TableSize = qcowClusterSize / qcowL2TableEntrySize;

			// get offset of L1 table
			final long qcowL1TableOffset = DiskImageUtils.readLong( diskFile, 40 );

			// check for each L2 table referenced from an L1 table its entries
			// until a compressed cluster descriptor is found
			for ( long i = 0; i < qcowL1TableSize; i++ ) {
				// get offset of current L2 table from the current L1 table entry
				final long qcowL1TableEntryOffset = qcowL1TableOffset + ( i * qcowL1TableEntrySize );
				final long qcowL1TableEntry = DiskImageUtils.readLong( diskFile, qcowL1TableEntryOffset );

				// extract offset (bits 9 - 55) from L1 table entry
				final long qcowL2TableOffset = ( qcowL1TableEntry & 0x00fffffffffffe00L );

				if ( qcowL2TableOffset == 0 ) {
					// L2 table and all clusters described by this L2 table are unallocated
					continue;
				}

				// get each L2 table entry and check if it is a compressed cluster descriptor
				for ( long j = 0; j < qcowL2TableSize; j++ ) {
					// get current L2 table entry
					final long qcowL2TableEntryOffset = qcowL2TableOffset + ( j * qcowL2TableEntrySize );
					final long qcowL2TableEntry = DiskImageUtils.readLong( diskFile, qcowL2TableEntryOffset );

					// extract cluster type (standard or compressed) (bit 62)
					boolean qcowClusterCompressed = ( ( ( qcowL2TableEntry & 0x4000000000000000L ) >>> 62 ) == 1 );

					// check if QCOW2 disk image contains at least one compressed cluster descriptor
					if ( qcowClusterCompressed ) {
						qcowCompressed = true;
						break;
					}
				}

				// terminate if one compressed cluster descriptor is already found
				if ( qcowCompressed ) {
					break;
				}
			}
		} else {
			// QCOW2 image does not contain an active L1 table with any entry: l1_size = 0
			qcowCompressed = false;
		}

		return qcowCompressed;
	}

	@Override
	public boolean isSnapshot() throws DiskImageException
	{
		final RandomAccessFile diskFile = this.getDiskImage();
		final int qcowNumSnapshots = DiskImageUtils.readInt( diskFile, 56 );
		final boolean qcowSnapshot;

		// check if QCOW2 image contains at least one snapshot
		if ( qcowNumSnapshots == 0 ) {
			qcowSnapshot = true;
		} else {
			qcowSnapshot = false;
		}

		return qcowSnapshot;
	}

	@Override
	public Version getVersion() throws DiskImageException
	{
		final RandomAccessFile diskFile = this.getDiskImage();
		final int qcowVersion = DiskImageUtils.readInt( diskFile, 4 );

		// check QCOW2 file format version
		if ( qcowVersion < 2 || qcowVersion > 3 ) {
			// QCOW2 disk image does not contain a valid QCOW2 version
			final String errorMsg = "Invalid QCOW2 version in header found!";
			throw new DiskImageException( errorMsg );
		}

		return new Version( Integer.valueOf( qcowVersion ).shortValue() );
	}

	@Override
	public String getDescription() throws DiskImageException
	{
		// QCOW2 disk image format does not support any disk description
		return null;
	}

	@Override
	public ImageFormat getFormat()
	{
		return ImageFormat.QCOW2;
	}
}