1 """ This module implements python API for the FIEMAP ioctl. The FIEMAP ioctl
2 allows to find holes and mapped areas in a file. """
4 # Note, a lot of code in this module is not very readable, because it deals
5 # with the rather complex FIEMAP ioctl. To understand the code, you need to
6 # know the FIEMAP interface, which is documented in the
7 # Documentation/filesystems/fiemap.txt file in the Linux kernel sources.
9 # Disable the following pylint recommendations:
10 # * Too many instance attributes (R0902)
11 # pylint: disable=R0902
17 from mic.utils.misc import get_block_size
19 # Format string for 'struct fiemap'
20 _FIEMAP_FORMAT = "=QQLLLL"
21 # sizeof(struct fiemap)
22 _FIEMAP_SIZE = struct.calcsize(_FIEMAP_FORMAT)
23 # Format string for 'struct fiemap_extent'
24 _FIEMAP_EXTENT_FORMAT = "=QQQQQLLLL"
25 # sizeof(struct fiemap_extent)
26 _FIEMAP_EXTENT_SIZE = struct.calcsize(_FIEMAP_EXTENT_FORMAT)
27 # The FIEMAP ioctl number
28 _FIEMAP_IOCTL = 0xC020660B
30 # Minimum buffer which is required for 'class Fiemap' to operate
31 MIN_BUFFER_SIZE = _FIEMAP_SIZE + _FIEMAP_EXTENT_SIZE
32 # The default buffer size for 'class Fiemap'
33 DEFAULT_BUFFER_SIZE = 256 * 1024
35 class Error(Exception):
36 """ A class for exceptions generated by this module. We currently support
37 only one type of exceptions, and we basically throw human-readable problem
38 description in case of errors. """
42 """ This class provides API to the FIEMAP ioctl. Namely, it allows to
43 iterate over all mapped blocks and over all holes. """
45 def _open_image_file(self):
46 """ Open the image file. """
49 self._f_image = open(self._image_path, 'rb')
50 except IOError as err:
51 raise Error("cannot open image file '%s': %s" \
52 % (self._image_path, err))
54 self._f_image_needs_close = True
56 def __init__(self, image, buf_size = DEFAULT_BUFFER_SIZE):
57 """ Initialize a class instance. The 'image' argument is full path to
58 the file to operate on, or a file object to operate on.
60 The 'buf_size' argument is the size of the buffer for 'struct
61 fiemap_extent' elements which will be used when invoking the FIEMAP
62 ioctl. The larger is the buffer, the less times the FIEMAP ioctl will
65 self._f_image_needs_close = False
67 if hasattr(image, "fileno"):
69 self._image_path = image.name
71 self._image_path = image
72 self._open_image_file()
75 if buf_size < MIN_BUFFER_SIZE:
76 raise Error("too small buffer (%d bytes), minimum is %d bytes" \
77 % (buf_size, MIN_BUFFER_SIZE))
79 # How many 'struct fiemap_extent' elements fit the buffer
80 buf_size -= _FIEMAP_SIZE
81 self._fiemap_extent_cnt = buf_size / _FIEMAP_EXTENT_SIZE
82 self._buf_size = self._fiemap_extent_cnt * _FIEMAP_EXTENT_SIZE
83 self._buf_size += _FIEMAP_SIZE
85 # Allocate a mutable buffer for the FIEMAP ioctl
86 self._buf = array.array('B', [0] * self._buf_size)
88 self.image_size = os.fstat(self._f_image.fileno()).st_size
91 self.block_size = get_block_size(self._f_image)
92 except IOError as err:
93 raise Error("cannot get block size for '%s': %s" \
94 % (self._image_path, err))
96 self.blocks_cnt = self.image_size + self.block_size - 1
97 self.blocks_cnt /= self.block_size
99 # Synchronize the image file to make sure FIEMAP returns correct values
101 self._f_image.flush()
102 except IOError as err:
103 raise Error("cannot flush image file '%s': %s" \
104 % (self._image_path, err))
106 os.fsync(self._f_image.fileno()),
107 except OSError as err:
108 raise Error("cannot synchronize image file '%s': %s " \
109 % (self._image_path, err.strerror))
111 # Check if the FIEMAP ioctl is supported
112 self.block_is_mapped(0)
115 """ The class destructor which closes the opened files. """
117 if self._f_image_needs_close:
118 self._f_image.close()
120 def _invoke_fiemap(self, block, count):
121 """ Invoke the FIEMAP ioctl for 'count' blocks of the file starting from
122 block number 'block'.
124 The full result of the operation is stored in 'self._buf' on exit.
125 Returns the unpacked 'struct fiemap' data structure in form of a python
126 list (just like 'struct.upack()'). """
128 if block < 0 or block >= self.blocks_cnt:
129 raise Error("bad block number %d, should be within [0, %d]" \
130 % (block, self.blocks_cnt))
132 # Initialize the 'struct fiemap' part of the buffer
133 struct.pack_into(_FIEMAP_FORMAT, self._buf, 0, block * self.block_size,
134 count * self.block_size, 0, 0,
135 self._fiemap_extent_cnt, 0)
138 fcntl.ioctl(self._f_image, _FIEMAP_IOCTL, self._buf, 1)
139 except IOError as err:
140 error_msg = "the FIEMAP ioctl failed for '%s': %s" \
141 % (self._image_path, err)
142 if err.errno == os.errno.EPERM or err.errno == os.errno.EACCES:
143 # The FIEMAP ioctl was added in kernel version 2.6.28 in 2008
144 error_msg += " (looks like your kernel does not support FIEMAP)"
146 raise Error(error_msg)
148 return struct.unpack(_FIEMAP_FORMAT, self._buf[:_FIEMAP_SIZE])
150 def block_is_mapped(self, block):
151 """ This function returns 'True' if block number 'block' of the image
152 file is mapped and 'False' otherwise. """
154 struct_fiemap = self._invoke_fiemap(block, 1)
156 # The 3rd element of 'struct_fiemap' is the 'fm_mapped_extents' field.
157 # If it contains zero, the block is not mapped, otherwise it is
159 return bool(struct_fiemap[3])
161 def block_is_unmapped(self, block):
162 """ This function returns 'True' if block number 'block' of the image
163 file is not mapped (hole) and 'False' otherwise. """
165 return not self.block_is_mapped(block)
167 def _unpack_fiemap_extent(self, index):
168 """ Unpack a 'struct fiemap_extent' structure object number 'index'
169 from the internal 'self._buf' buffer. """
171 offset = _FIEMAP_SIZE + _FIEMAP_EXTENT_SIZE * index
172 return struct.unpack(_FIEMAP_EXTENT_FORMAT,
173 self._buf[offset : offset + _FIEMAP_EXTENT_SIZE])
175 def _do_get_mapped_ranges(self, start, count):
176 """ Implements most the functionality for the 'get_mapped_ranges()'
177 generator: invokes the FIEMAP ioctl, walks through the mapped
178 extents and yields mapped block ranges. However, the ranges may be
179 consecutive (e.g., (1, 100), (100, 200)) and 'get_mapped_ranges()'
180 simply merges them. """
183 while block < start + count:
184 struct_fiemap = self._invoke_fiemap(block, count)
186 mapped_extents = struct_fiemap[3]
187 if mapped_extents == 0:
188 # No more mapped blocks
192 while extent < mapped_extents:
193 fiemap_extent = self._unpack_fiemap_extent(extent)
195 # Start of the extent
196 extent_start = fiemap_extent[0]
197 # Starting block number of the extent
198 extent_block = extent_start / self.block_size
199 # Length of the extent
200 extent_len = fiemap_extent[2]
201 # Count of blocks in the extent
202 extent_count = extent_len / self.block_size
204 # Extent length and offset have to be block-aligned
205 assert extent_start % self.block_size == 0
206 assert extent_len % self.block_size == 0
208 if extent_block > start + count - 1:
211 first = max(extent_block, block)
212 last = min(extent_block + extent_count, start + count) - 1
217 block = extent_block + extent_count
219 def get_mapped_ranges(self, start, count):
220 """ A generator which yields ranges of mapped blocks in the file. The
221 ranges are tuples of 2 elements: [first, last], where 'first' is the
222 first mapped block and 'last' is the last mapped block.
224 The ranges are yielded for the area of the file of size 'count' blocks,
225 starting from block 'start'. """
227 iterator = self._do_get_mapped_ranges(start, count)
229 first_prev, last_prev = iterator.next()
231 for first, last in iterator:
232 if last_prev == first - 1:
235 yield (first_prev, last_prev)
236 first_prev, last_prev = first, last
238 yield (first_prev, last_prev)
240 def get_unmapped_ranges(self, start, count):
241 """ Just like 'get_mapped_ranges()', but yields unmapped block ranges
245 for first, last in self._do_get_mapped_ranges(start, count):
246 if first > hole_first:
247 yield (hole_first, first - 1)
249 hole_first = last + 1
251 if hole_first < start + count:
252 yield (hole_first, start + count - 1)