1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# Copyright (c) 2012 Intel, Inc. 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun 7*4882a593Smuzhiyun""" 8*4882a593SmuzhiyunThis module implements python implements a way to get file block. Two methods 9*4882a593Smuzhiyunare supported - the FIEMAP ioctl and the 'SEEK_HOLE / SEEK_DATA' features of 10*4882a593Smuzhiyunthe file seek syscall. The former is implemented by the 'FilemapFiemap' class, 11*4882a593Smuzhiyunthe latter is implemented by the 'FilemapSeek' class. Both classes provide the 12*4882a593Smuzhiyunsame API. The 'filemap' function automatically selects which class can be used 13*4882a593Smuzhiyunand returns an instance of the class. 14*4882a593Smuzhiyun""" 15*4882a593Smuzhiyun 16*4882a593Smuzhiyun# Disable the following pylint recommendations: 17*4882a593Smuzhiyun# * Too many instance attributes (R0902) 18*4882a593Smuzhiyun# pylint: disable=R0902 19*4882a593Smuzhiyun 20*4882a593Smuzhiyunimport errno 21*4882a593Smuzhiyunimport os 22*4882a593Smuzhiyunimport struct 23*4882a593Smuzhiyunimport array 24*4882a593Smuzhiyunimport fcntl 25*4882a593Smuzhiyunimport tempfile 26*4882a593Smuzhiyunimport logging 27*4882a593Smuzhiyun 28*4882a593Smuzhiyundef get_block_size(file_obj): 29*4882a593Smuzhiyun """ 30*4882a593Smuzhiyun Returns block size for file object 'file_obj'. Errors are indicated by the 31*4882a593Smuzhiyun 'IOError' exception. 32*4882a593Smuzhiyun """ 33*4882a593Smuzhiyun # Get the block size of the host file-system for the image file by calling 34*4882a593Smuzhiyun # the FIGETBSZ ioctl (number 2). 35*4882a593Smuzhiyun try: 36*4882a593Smuzhiyun binary_data = fcntl.ioctl(file_obj, 2, struct.pack('I', 0)) 37*4882a593Smuzhiyun bsize = struct.unpack('I', binary_data)[0] 38*4882a593Smuzhiyun except OSError: 39*4882a593Smuzhiyun bsize = None 40*4882a593Smuzhiyun 41*4882a593Smuzhiyun # If ioctl causes OSError or give bsize to zero failback to os.fstat 42*4882a593Smuzhiyun if not bsize: 43*4882a593Smuzhiyun import os 44*4882a593Smuzhiyun stat = os.fstat(file_obj.fileno()) 45*4882a593Smuzhiyun if hasattr(stat, 'st_blksize'): 46*4882a593Smuzhiyun bsize = stat.st_blksize 47*4882a593Smuzhiyun else: 48*4882a593Smuzhiyun raise IOError("Unable to determine block size") 49*4882a593Smuzhiyun return bsize 50*4882a593Smuzhiyun 51*4882a593Smuzhiyunclass ErrorNotSupp(Exception): 52*4882a593Smuzhiyun """ 53*4882a593Smuzhiyun An exception of this type is raised when the 'FIEMAP' or 'SEEK_HOLE' feature 54*4882a593Smuzhiyun is not supported either by the kernel or the file-system. 55*4882a593Smuzhiyun """ 56*4882a593Smuzhiyun pass 57*4882a593Smuzhiyun 58*4882a593Smuzhiyunclass Error(Exception): 59*4882a593Smuzhiyun """A class for all the other exceptions raised by this module.""" 60*4882a593Smuzhiyun pass 61*4882a593Smuzhiyun 62*4882a593Smuzhiyun 63*4882a593Smuzhiyunclass _FilemapBase(object): 64*4882a593Smuzhiyun """ 65*4882a593Smuzhiyun This is a base class for a couple of other classes in this module. This 66*4882a593Smuzhiyun class simply performs the common parts of the initialization process: opens 67*4882a593Smuzhiyun the image file, gets its size, etc. The 'log' parameter is the logger object 68*4882a593Smuzhiyun to use for printing messages. 69*4882a593Smuzhiyun """ 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun def __init__(self, image, log=None): 72*4882a593Smuzhiyun """ 73*4882a593Smuzhiyun Initialize a class instance. The 'image' argument is full path to the 74*4882a593Smuzhiyun file or file object to operate on. 75*4882a593Smuzhiyun """ 76*4882a593Smuzhiyun 77*4882a593Smuzhiyun self._log = log 78*4882a593Smuzhiyun if self._log is None: 79*4882a593Smuzhiyun self._log = logging.getLogger(__name__) 80*4882a593Smuzhiyun 81*4882a593Smuzhiyun self._f_image_needs_close = False 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun if hasattr(image, "fileno"): 84*4882a593Smuzhiyun self._f_image = image 85*4882a593Smuzhiyun self._image_path = image.name 86*4882a593Smuzhiyun else: 87*4882a593Smuzhiyun self._image_path = image 88*4882a593Smuzhiyun self._open_image_file() 89*4882a593Smuzhiyun 90*4882a593Smuzhiyun try: 91*4882a593Smuzhiyun self.image_size = os.fstat(self._f_image.fileno()).st_size 92*4882a593Smuzhiyun except IOError as err: 93*4882a593Smuzhiyun raise Error("cannot get information about file '%s': %s" 94*4882a593Smuzhiyun % (self._f_image.name, err)) 95*4882a593Smuzhiyun 96*4882a593Smuzhiyun try: 97*4882a593Smuzhiyun self.block_size = get_block_size(self._f_image) 98*4882a593Smuzhiyun except IOError as err: 99*4882a593Smuzhiyun raise Error("cannot get block size for '%s': %s" 100*4882a593Smuzhiyun % (self._image_path, err)) 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun self.blocks_cnt = self.image_size + self.block_size - 1 103*4882a593Smuzhiyun self.blocks_cnt //= self.block_size 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun try: 106*4882a593Smuzhiyun self._f_image.flush() 107*4882a593Smuzhiyun except IOError as err: 108*4882a593Smuzhiyun raise Error("cannot flush image file '%s': %s" 109*4882a593Smuzhiyun % (self._image_path, err)) 110*4882a593Smuzhiyun 111*4882a593Smuzhiyun try: 112*4882a593Smuzhiyun os.fsync(self._f_image.fileno()), 113*4882a593Smuzhiyun except OSError as err: 114*4882a593Smuzhiyun raise Error("cannot synchronize image file '%s': %s " 115*4882a593Smuzhiyun % (self._image_path, err.strerror)) 116*4882a593Smuzhiyun 117*4882a593Smuzhiyun self._log.debug("opened image \"%s\"" % self._image_path) 118*4882a593Smuzhiyun self._log.debug("block size %d, blocks count %d, image size %d" 119*4882a593Smuzhiyun % (self.block_size, self.blocks_cnt, self.image_size)) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun def __del__(self): 122*4882a593Smuzhiyun """The class destructor which just closes the image file.""" 123*4882a593Smuzhiyun if self._f_image_needs_close: 124*4882a593Smuzhiyun self._f_image.close() 125*4882a593Smuzhiyun 126*4882a593Smuzhiyun def _open_image_file(self): 127*4882a593Smuzhiyun """Open the image file.""" 128*4882a593Smuzhiyun try: 129*4882a593Smuzhiyun self._f_image = open(self._image_path, 'rb') 130*4882a593Smuzhiyun except IOError as err: 131*4882a593Smuzhiyun raise Error("cannot open image file '%s': %s" 132*4882a593Smuzhiyun % (self._image_path, err)) 133*4882a593Smuzhiyun 134*4882a593Smuzhiyun self._f_image_needs_close = True 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun def block_is_mapped(self, block): # pylint: disable=W0613,R0201 137*4882a593Smuzhiyun """ 138*4882a593Smuzhiyun This method has has to be implemented by child classes. It returns 139*4882a593Smuzhiyun 'True' if block number 'block' of the image file is mapped and 'False' 140*4882a593Smuzhiyun otherwise. 141*4882a593Smuzhiyun """ 142*4882a593Smuzhiyun 143*4882a593Smuzhiyun raise Error("the method is not implemented") 144*4882a593Smuzhiyun 145*4882a593Smuzhiyun def get_mapped_ranges(self, start, count): # pylint: disable=W0613,R0201 146*4882a593Smuzhiyun """ 147*4882a593Smuzhiyun This method has has to be implemented by child classes. This is a 148*4882a593Smuzhiyun generator which yields ranges of mapped blocks in the file. The ranges 149*4882a593Smuzhiyun are tuples of 2 elements: [first, last], where 'first' is the first 150*4882a593Smuzhiyun mapped block and 'last' is the last mapped block. 151*4882a593Smuzhiyun 152*4882a593Smuzhiyun The ranges are yielded for the area of the file of size 'count' blocks, 153*4882a593Smuzhiyun starting from block 'start'. 154*4882a593Smuzhiyun """ 155*4882a593Smuzhiyun 156*4882a593Smuzhiyun raise Error("the method is not implemented") 157*4882a593Smuzhiyun 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun# The 'SEEK_HOLE' and 'SEEK_DATA' options of the file seek system call 160*4882a593Smuzhiyun_SEEK_DATA = 3 161*4882a593Smuzhiyun_SEEK_HOLE = 4 162*4882a593Smuzhiyun 163*4882a593Smuzhiyundef _lseek(file_obj, offset, whence): 164*4882a593Smuzhiyun """This is a helper function which invokes 'os.lseek' for file object 165*4882a593Smuzhiyun 'file_obj' and with specified 'offset' and 'whence'. The 'whence' 166*4882a593Smuzhiyun argument is supposed to be either '_SEEK_DATA' or '_SEEK_HOLE'. When 167*4882a593Smuzhiyun there is no more data or hole starting from 'offset', this function 168*4882a593Smuzhiyun returns '-1'. Otherwise the data or hole position is returned.""" 169*4882a593Smuzhiyun 170*4882a593Smuzhiyun try: 171*4882a593Smuzhiyun return os.lseek(file_obj.fileno(), offset, whence) 172*4882a593Smuzhiyun except OSError as err: 173*4882a593Smuzhiyun # The 'lseek' system call returns the ENXIO if there is no data or 174*4882a593Smuzhiyun # hole starting from the specified offset. 175*4882a593Smuzhiyun if err.errno == errno.ENXIO: 176*4882a593Smuzhiyun return -1 177*4882a593Smuzhiyun elif err.errno == errno.EINVAL: 178*4882a593Smuzhiyun raise ErrorNotSupp("the kernel or file-system does not support " 179*4882a593Smuzhiyun "\"SEEK_HOLE\" and \"SEEK_DATA\"") 180*4882a593Smuzhiyun else: 181*4882a593Smuzhiyun raise 182*4882a593Smuzhiyun 183*4882a593Smuzhiyunclass FilemapSeek(_FilemapBase): 184*4882a593Smuzhiyun """ 185*4882a593Smuzhiyun This class uses the 'SEEK_HOLE' and 'SEEK_DATA' to find file block mapping. 186*4882a593Smuzhiyun Unfortunately, the current implementation requires the caller to have write 187*4882a593Smuzhiyun access to the image file. 188*4882a593Smuzhiyun """ 189*4882a593Smuzhiyun 190*4882a593Smuzhiyun def __init__(self, image, log=None): 191*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 192*4882a593Smuzhiyun 193*4882a593Smuzhiyun # Call the base class constructor first 194*4882a593Smuzhiyun _FilemapBase.__init__(self, image, log) 195*4882a593Smuzhiyun self._log.debug("FilemapSeek: initializing") 196*4882a593Smuzhiyun 197*4882a593Smuzhiyun self._probe_seek_hole() 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun def _probe_seek_hole(self): 200*4882a593Smuzhiyun """ 201*4882a593Smuzhiyun Check whether the system implements 'SEEK_HOLE' and 'SEEK_DATA'. 202*4882a593Smuzhiyun Unfortunately, there seems to be no clean way for detecting this, 203*4882a593Smuzhiyun because often the system just fakes them by just assuming that all 204*4882a593Smuzhiyun files are fully mapped, so 'SEEK_HOLE' always returns EOF and 205*4882a593Smuzhiyun 'SEEK_DATA' always returns the requested offset. 206*4882a593Smuzhiyun 207*4882a593Smuzhiyun I could not invent a better way of detecting the fake 'SEEK_HOLE' 208*4882a593Smuzhiyun implementation than just to create a temporary file in the same 209*4882a593Smuzhiyun directory where the image file resides. It would be nice to change this 210*4882a593Smuzhiyun to something better. 211*4882a593Smuzhiyun """ 212*4882a593Smuzhiyun 213*4882a593Smuzhiyun directory = os.path.dirname(self._image_path) 214*4882a593Smuzhiyun 215*4882a593Smuzhiyun try: 216*4882a593Smuzhiyun tmp_obj = tempfile.TemporaryFile("w+", dir=directory) 217*4882a593Smuzhiyun except IOError as err: 218*4882a593Smuzhiyun raise ErrorNotSupp("cannot create a temporary in \"%s\": %s" \ 219*4882a593Smuzhiyun % (directory, err)) 220*4882a593Smuzhiyun 221*4882a593Smuzhiyun try: 222*4882a593Smuzhiyun os.ftruncate(tmp_obj.fileno(), self.block_size) 223*4882a593Smuzhiyun except OSError as err: 224*4882a593Smuzhiyun raise ErrorNotSupp("cannot truncate temporary file in \"%s\": %s" 225*4882a593Smuzhiyun % (directory, err)) 226*4882a593Smuzhiyun 227*4882a593Smuzhiyun offs = _lseek(tmp_obj, 0, _SEEK_HOLE) 228*4882a593Smuzhiyun if offs != 0: 229*4882a593Smuzhiyun # We are dealing with the stub 'SEEK_HOLE' implementation which 230*4882a593Smuzhiyun # always returns EOF. 231*4882a593Smuzhiyun self._log.debug("lseek(0, SEEK_HOLE) returned %d" % offs) 232*4882a593Smuzhiyun raise ErrorNotSupp("the file-system does not support " 233*4882a593Smuzhiyun "\"SEEK_HOLE\" and \"SEEK_DATA\" but only " 234*4882a593Smuzhiyun "provides a stub implementation") 235*4882a593Smuzhiyun 236*4882a593Smuzhiyun tmp_obj.close() 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun def block_is_mapped(self, block): 239*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 240*4882a593Smuzhiyun offs = _lseek(self._f_image, block * self.block_size, _SEEK_DATA) 241*4882a593Smuzhiyun if offs == -1: 242*4882a593Smuzhiyun result = False 243*4882a593Smuzhiyun else: 244*4882a593Smuzhiyun result = (offs // self.block_size == block) 245*4882a593Smuzhiyun 246*4882a593Smuzhiyun self._log.debug("FilemapSeek: block_is_mapped(%d) returns %s" 247*4882a593Smuzhiyun % (block, result)) 248*4882a593Smuzhiyun return result 249*4882a593Smuzhiyun 250*4882a593Smuzhiyun def _get_ranges(self, start, count, whence1, whence2): 251*4882a593Smuzhiyun """ 252*4882a593Smuzhiyun This function implements 'get_mapped_ranges()' depending 253*4882a593Smuzhiyun on what is passed in the 'whence1' and 'whence2' arguments. 254*4882a593Smuzhiyun """ 255*4882a593Smuzhiyun 256*4882a593Smuzhiyun assert whence1 != whence2 257*4882a593Smuzhiyun end = start * self.block_size 258*4882a593Smuzhiyun limit = end + count * self.block_size 259*4882a593Smuzhiyun 260*4882a593Smuzhiyun while True: 261*4882a593Smuzhiyun start = _lseek(self._f_image, end, whence1) 262*4882a593Smuzhiyun if start == -1 or start >= limit or start == self.image_size: 263*4882a593Smuzhiyun break 264*4882a593Smuzhiyun 265*4882a593Smuzhiyun end = _lseek(self._f_image, start, whence2) 266*4882a593Smuzhiyun if end == -1 or end == self.image_size: 267*4882a593Smuzhiyun end = self.blocks_cnt * self.block_size 268*4882a593Smuzhiyun if end > limit: 269*4882a593Smuzhiyun end = limit 270*4882a593Smuzhiyun 271*4882a593Smuzhiyun start_blk = start // self.block_size 272*4882a593Smuzhiyun end_blk = end // self.block_size - 1 273*4882a593Smuzhiyun self._log.debug("FilemapSeek: yielding range (%d, %d)" 274*4882a593Smuzhiyun % (start_blk, end_blk)) 275*4882a593Smuzhiyun yield (start_blk, end_blk) 276*4882a593Smuzhiyun 277*4882a593Smuzhiyun def get_mapped_ranges(self, start, count): 278*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 279*4882a593Smuzhiyun self._log.debug("FilemapSeek: get_mapped_ranges(%d, %d(%d))" 280*4882a593Smuzhiyun % (start, count, start + count - 1)) 281*4882a593Smuzhiyun return self._get_ranges(start, count, _SEEK_DATA, _SEEK_HOLE) 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun 284*4882a593Smuzhiyun# Below goes the FIEMAP ioctl implementation, which is not very readable 285*4882a593Smuzhiyun# because it deals with the rather complex FIEMAP ioctl. To understand the 286*4882a593Smuzhiyun# code, you need to know the FIEMAP interface, which is documented in the 287*4882a593Smuzhiyun# "Documentation/filesystems/fiemap.txt" file in the Linux kernel sources. 288*4882a593Smuzhiyun 289*4882a593Smuzhiyun# Format string for 'struct fiemap' 290*4882a593Smuzhiyun_FIEMAP_FORMAT = "=QQLLLL" 291*4882a593Smuzhiyun# sizeof(struct fiemap) 292*4882a593Smuzhiyun_FIEMAP_SIZE = struct.calcsize(_FIEMAP_FORMAT) 293*4882a593Smuzhiyun# Format string for 'struct fiemap_extent' 294*4882a593Smuzhiyun_FIEMAP_EXTENT_FORMAT = "=QQQQQLLLL" 295*4882a593Smuzhiyun# sizeof(struct fiemap_extent) 296*4882a593Smuzhiyun_FIEMAP_EXTENT_SIZE = struct.calcsize(_FIEMAP_EXTENT_FORMAT) 297*4882a593Smuzhiyun# The FIEMAP ioctl number 298*4882a593Smuzhiyun_FIEMAP_IOCTL = 0xC020660B 299*4882a593Smuzhiyun# This FIEMAP ioctl flag which instructs the kernel to sync the file before 300*4882a593Smuzhiyun# reading the block map 301*4882a593Smuzhiyun_FIEMAP_FLAG_SYNC = 0x00000001 302*4882a593Smuzhiyun# Size of the buffer for 'struct fiemap_extent' elements which will be used 303*4882a593Smuzhiyun# when invoking the FIEMAP ioctl. The larger is the buffer, the less times the 304*4882a593Smuzhiyun# FIEMAP ioctl will be invoked. 305*4882a593Smuzhiyun_FIEMAP_BUFFER_SIZE = 256 * 1024 306*4882a593Smuzhiyun 307*4882a593Smuzhiyunclass FilemapFiemap(_FilemapBase): 308*4882a593Smuzhiyun """ 309*4882a593Smuzhiyun This class provides API to the FIEMAP ioctl. Namely, it allows to iterate 310*4882a593Smuzhiyun over all mapped blocks and over all holes. 311*4882a593Smuzhiyun 312*4882a593Smuzhiyun This class synchronizes the image file every time it invokes the FIEMAP 313*4882a593Smuzhiyun ioctl in order to work-around early FIEMAP implementation kernel bugs. 314*4882a593Smuzhiyun """ 315*4882a593Smuzhiyun 316*4882a593Smuzhiyun def __init__(self, image, log=None): 317*4882a593Smuzhiyun """ 318*4882a593Smuzhiyun Initialize a class instance. The 'image' argument is full the file 319*4882a593Smuzhiyun object to operate on. 320*4882a593Smuzhiyun """ 321*4882a593Smuzhiyun 322*4882a593Smuzhiyun # Call the base class constructor first 323*4882a593Smuzhiyun _FilemapBase.__init__(self, image, log) 324*4882a593Smuzhiyun self._log.debug("FilemapFiemap: initializing") 325*4882a593Smuzhiyun 326*4882a593Smuzhiyun self._buf_size = _FIEMAP_BUFFER_SIZE 327*4882a593Smuzhiyun 328*4882a593Smuzhiyun # Calculate how many 'struct fiemap_extent' elements fit the buffer 329*4882a593Smuzhiyun self._buf_size -= _FIEMAP_SIZE 330*4882a593Smuzhiyun self._fiemap_extent_cnt = self._buf_size // _FIEMAP_EXTENT_SIZE 331*4882a593Smuzhiyun assert self._fiemap_extent_cnt > 0 332*4882a593Smuzhiyun self._buf_size = self._fiemap_extent_cnt * _FIEMAP_EXTENT_SIZE 333*4882a593Smuzhiyun self._buf_size += _FIEMAP_SIZE 334*4882a593Smuzhiyun 335*4882a593Smuzhiyun # Allocate a mutable buffer for the FIEMAP ioctl 336*4882a593Smuzhiyun self._buf = array.array('B', [0] * self._buf_size) 337*4882a593Smuzhiyun 338*4882a593Smuzhiyun # Check if the FIEMAP ioctl is supported 339*4882a593Smuzhiyun self.block_is_mapped(0) 340*4882a593Smuzhiyun 341*4882a593Smuzhiyun def _invoke_fiemap(self, block, count): 342*4882a593Smuzhiyun """ 343*4882a593Smuzhiyun Invoke the FIEMAP ioctl for 'count' blocks of the file starting from 344*4882a593Smuzhiyun block number 'block'. 345*4882a593Smuzhiyun 346*4882a593Smuzhiyun The full result of the operation is stored in 'self._buf' on exit. 347*4882a593Smuzhiyun Returns the unpacked 'struct fiemap' data structure in form of a python 348*4882a593Smuzhiyun list (just like 'struct.upack()'). 349*4882a593Smuzhiyun """ 350*4882a593Smuzhiyun 351*4882a593Smuzhiyun if self.blocks_cnt != 0 and (block < 0 or block >= self.blocks_cnt): 352*4882a593Smuzhiyun raise Error("bad block number %d, should be within [0, %d]" 353*4882a593Smuzhiyun % (block, self.blocks_cnt)) 354*4882a593Smuzhiyun 355*4882a593Smuzhiyun # Initialize the 'struct fiemap' part of the buffer. We use the 356*4882a593Smuzhiyun # '_FIEMAP_FLAG_SYNC' flag in order to make sure the file is 357*4882a593Smuzhiyun # synchronized. The reason for this is that early FIEMAP 358*4882a593Smuzhiyun # implementations had many bugs related to cached dirty data, and 359*4882a593Smuzhiyun # synchronizing the file is a necessary work-around. 360*4882a593Smuzhiyun struct.pack_into(_FIEMAP_FORMAT, self._buf, 0, block * self.block_size, 361*4882a593Smuzhiyun count * self.block_size, _FIEMAP_FLAG_SYNC, 0, 362*4882a593Smuzhiyun self._fiemap_extent_cnt, 0) 363*4882a593Smuzhiyun 364*4882a593Smuzhiyun try: 365*4882a593Smuzhiyun fcntl.ioctl(self._f_image, _FIEMAP_IOCTL, self._buf, 1) 366*4882a593Smuzhiyun except IOError as err: 367*4882a593Smuzhiyun # Note, the FIEMAP ioctl is supported by the Linux kernel starting 368*4882a593Smuzhiyun # from version 2.6.28 (year 2008). 369*4882a593Smuzhiyun if err.errno == errno.EOPNOTSUPP: 370*4882a593Smuzhiyun errstr = "FilemapFiemap: the FIEMAP ioctl is not supported " \ 371*4882a593Smuzhiyun "by the file-system" 372*4882a593Smuzhiyun self._log.debug(errstr) 373*4882a593Smuzhiyun raise ErrorNotSupp(errstr) 374*4882a593Smuzhiyun if err.errno == errno.ENOTTY: 375*4882a593Smuzhiyun errstr = "FilemapFiemap: the FIEMAP ioctl is not supported " \ 376*4882a593Smuzhiyun "by the kernel" 377*4882a593Smuzhiyun self._log.debug(errstr) 378*4882a593Smuzhiyun raise ErrorNotSupp(errstr) 379*4882a593Smuzhiyun raise Error("the FIEMAP ioctl failed for '%s': %s" 380*4882a593Smuzhiyun % (self._image_path, err)) 381*4882a593Smuzhiyun 382*4882a593Smuzhiyun return struct.unpack(_FIEMAP_FORMAT, self._buf[:_FIEMAP_SIZE]) 383*4882a593Smuzhiyun 384*4882a593Smuzhiyun def block_is_mapped(self, block): 385*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 386*4882a593Smuzhiyun struct_fiemap = self._invoke_fiemap(block, 1) 387*4882a593Smuzhiyun 388*4882a593Smuzhiyun # The 3rd element of 'struct_fiemap' is the 'fm_mapped_extents' field. 389*4882a593Smuzhiyun # If it contains zero, the block is not mapped, otherwise it is 390*4882a593Smuzhiyun # mapped. 391*4882a593Smuzhiyun result = bool(struct_fiemap[3]) 392*4882a593Smuzhiyun self._log.debug("FilemapFiemap: block_is_mapped(%d) returns %s" 393*4882a593Smuzhiyun % (block, result)) 394*4882a593Smuzhiyun return result 395*4882a593Smuzhiyun 396*4882a593Smuzhiyun def _unpack_fiemap_extent(self, index): 397*4882a593Smuzhiyun """ 398*4882a593Smuzhiyun Unpack a 'struct fiemap_extent' structure object number 'index' from 399*4882a593Smuzhiyun the internal 'self._buf' buffer. 400*4882a593Smuzhiyun """ 401*4882a593Smuzhiyun 402*4882a593Smuzhiyun offset = _FIEMAP_SIZE + _FIEMAP_EXTENT_SIZE * index 403*4882a593Smuzhiyun return struct.unpack(_FIEMAP_EXTENT_FORMAT, 404*4882a593Smuzhiyun self._buf[offset : offset + _FIEMAP_EXTENT_SIZE]) 405*4882a593Smuzhiyun 406*4882a593Smuzhiyun def _do_get_mapped_ranges(self, start, count): 407*4882a593Smuzhiyun """ 408*4882a593Smuzhiyun Implements most the functionality for the 'get_mapped_ranges()' 409*4882a593Smuzhiyun generator: invokes the FIEMAP ioctl, walks through the mapped extents 410*4882a593Smuzhiyun and yields mapped block ranges. However, the ranges may be consecutive 411*4882a593Smuzhiyun (e.g., (1, 100), (100, 200)) and 'get_mapped_ranges()' simply merges 412*4882a593Smuzhiyun them. 413*4882a593Smuzhiyun """ 414*4882a593Smuzhiyun 415*4882a593Smuzhiyun block = start 416*4882a593Smuzhiyun while block < start + count: 417*4882a593Smuzhiyun struct_fiemap = self._invoke_fiemap(block, count) 418*4882a593Smuzhiyun 419*4882a593Smuzhiyun mapped_extents = struct_fiemap[3] 420*4882a593Smuzhiyun if mapped_extents == 0: 421*4882a593Smuzhiyun # No more mapped blocks 422*4882a593Smuzhiyun return 423*4882a593Smuzhiyun 424*4882a593Smuzhiyun extent = 0 425*4882a593Smuzhiyun while extent < mapped_extents: 426*4882a593Smuzhiyun fiemap_extent = self._unpack_fiemap_extent(extent) 427*4882a593Smuzhiyun 428*4882a593Smuzhiyun # Start of the extent 429*4882a593Smuzhiyun extent_start = fiemap_extent[0] 430*4882a593Smuzhiyun # Starting block number of the extent 431*4882a593Smuzhiyun extent_block = extent_start // self.block_size 432*4882a593Smuzhiyun # Length of the extent 433*4882a593Smuzhiyun extent_len = fiemap_extent[2] 434*4882a593Smuzhiyun # Count of blocks in the extent 435*4882a593Smuzhiyun extent_count = extent_len // self.block_size 436*4882a593Smuzhiyun 437*4882a593Smuzhiyun # Extent length and offset have to be block-aligned 438*4882a593Smuzhiyun assert extent_start % self.block_size == 0 439*4882a593Smuzhiyun assert extent_len % self.block_size == 0 440*4882a593Smuzhiyun 441*4882a593Smuzhiyun if extent_block > start + count - 1: 442*4882a593Smuzhiyun return 443*4882a593Smuzhiyun 444*4882a593Smuzhiyun first = max(extent_block, block) 445*4882a593Smuzhiyun last = min(extent_block + extent_count, start + count) - 1 446*4882a593Smuzhiyun yield (first, last) 447*4882a593Smuzhiyun 448*4882a593Smuzhiyun extent += 1 449*4882a593Smuzhiyun 450*4882a593Smuzhiyun block = extent_block + extent_count 451*4882a593Smuzhiyun 452*4882a593Smuzhiyun def get_mapped_ranges(self, start, count): 453*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 454*4882a593Smuzhiyun self._log.debug("FilemapFiemap: get_mapped_ranges(%d, %d(%d))" 455*4882a593Smuzhiyun % (start, count, start + count - 1)) 456*4882a593Smuzhiyun iterator = self._do_get_mapped_ranges(start, count) 457*4882a593Smuzhiyun first_prev, last_prev = next(iterator) 458*4882a593Smuzhiyun 459*4882a593Smuzhiyun for first, last in iterator: 460*4882a593Smuzhiyun if last_prev == first - 1: 461*4882a593Smuzhiyun last_prev = last 462*4882a593Smuzhiyun else: 463*4882a593Smuzhiyun self._log.debug("FilemapFiemap: yielding range (%d, %d)" 464*4882a593Smuzhiyun % (first_prev, last_prev)) 465*4882a593Smuzhiyun yield (first_prev, last_prev) 466*4882a593Smuzhiyun first_prev, last_prev = first, last 467*4882a593Smuzhiyun 468*4882a593Smuzhiyun self._log.debug("FilemapFiemap: yielding range (%d, %d)" 469*4882a593Smuzhiyun % (first_prev, last_prev)) 470*4882a593Smuzhiyun yield (first_prev, last_prev) 471*4882a593Smuzhiyun 472*4882a593Smuzhiyunclass FilemapNobmap(_FilemapBase): 473*4882a593Smuzhiyun """ 474*4882a593Smuzhiyun This class is used when both the 'SEEK_DATA/HOLE' and FIEMAP are not 475*4882a593Smuzhiyun supported by the filesystem or kernel. 476*4882a593Smuzhiyun """ 477*4882a593Smuzhiyun 478*4882a593Smuzhiyun def __init__(self, image, log=None): 479*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 480*4882a593Smuzhiyun 481*4882a593Smuzhiyun # Call the base class constructor first 482*4882a593Smuzhiyun _FilemapBase.__init__(self, image, log) 483*4882a593Smuzhiyun self._log.debug("FilemapNobmap: initializing") 484*4882a593Smuzhiyun 485*4882a593Smuzhiyun def block_is_mapped(self, block): 486*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 487*4882a593Smuzhiyun return True 488*4882a593Smuzhiyun 489*4882a593Smuzhiyun def get_mapped_ranges(self, start, count): 490*4882a593Smuzhiyun """Refer the '_FilemapBase' class for the documentation.""" 491*4882a593Smuzhiyun self._log.debug("FilemapNobmap: get_mapped_ranges(%d, %d(%d))" 492*4882a593Smuzhiyun % (start, count, start + count - 1)) 493*4882a593Smuzhiyun yield (start, start + count -1) 494*4882a593Smuzhiyun 495*4882a593Smuzhiyundef filemap(image, log=None): 496*4882a593Smuzhiyun """ 497*4882a593Smuzhiyun Create and return an instance of a Filemap class - 'FilemapFiemap' or 498*4882a593Smuzhiyun 'FilemapSeek', depending on what the system we run on supports. If the 499*4882a593Smuzhiyun FIEMAP ioctl is supported, an instance of the 'FilemapFiemap' class is 500*4882a593Smuzhiyun returned. Otherwise, if 'SEEK_HOLE' is supported an instance of the 501*4882a593Smuzhiyun 'FilemapSeek' class is returned. If none of these are supported, the 502*4882a593Smuzhiyun function generates an 'Error' type exception. 503*4882a593Smuzhiyun """ 504*4882a593Smuzhiyun 505*4882a593Smuzhiyun try: 506*4882a593Smuzhiyun return FilemapFiemap(image, log) 507*4882a593Smuzhiyun except ErrorNotSupp: 508*4882a593Smuzhiyun try: 509*4882a593Smuzhiyun return FilemapSeek(image, log) 510*4882a593Smuzhiyun except ErrorNotSupp: 511*4882a593Smuzhiyun return FilemapNobmap(image, log) 512*4882a593Smuzhiyun 513*4882a593Smuzhiyundef sparse_copy(src_fname, dst_fname, skip=0, seek=0, 514*4882a593Smuzhiyun length=0, api=None): 515*4882a593Smuzhiyun """ 516*4882a593Smuzhiyun Efficiently copy sparse file to or into another file. 517*4882a593Smuzhiyun 518*4882a593Smuzhiyun src_fname: path to source file 519*4882a593Smuzhiyun dst_fname: path to destination file 520*4882a593Smuzhiyun skip: skip N bytes at thestart of src 521*4882a593Smuzhiyun seek: seek N bytes from the start of dst 522*4882a593Smuzhiyun length: read N bytes from src and write them to dst 523*4882a593Smuzhiyun api: FilemapFiemap or FilemapSeek object 524*4882a593Smuzhiyun """ 525*4882a593Smuzhiyun if not api: 526*4882a593Smuzhiyun api = filemap 527*4882a593Smuzhiyun fmap = api(src_fname) 528*4882a593Smuzhiyun try: 529*4882a593Smuzhiyun dst_file = open(dst_fname, 'r+b') 530*4882a593Smuzhiyun except IOError: 531*4882a593Smuzhiyun dst_file = open(dst_fname, 'wb') 532*4882a593Smuzhiyun if length: 533*4882a593Smuzhiyun dst_size = length + seek 534*4882a593Smuzhiyun else: 535*4882a593Smuzhiyun dst_size = os.path.getsize(src_fname) + seek - skip 536*4882a593Smuzhiyun dst_file.truncate(dst_size) 537*4882a593Smuzhiyun 538*4882a593Smuzhiyun written = 0 539*4882a593Smuzhiyun for first, last in fmap.get_mapped_ranges(0, fmap.blocks_cnt): 540*4882a593Smuzhiyun start = first * fmap.block_size 541*4882a593Smuzhiyun end = (last + 1) * fmap.block_size 542*4882a593Smuzhiyun 543*4882a593Smuzhiyun if skip >= end: 544*4882a593Smuzhiyun continue 545*4882a593Smuzhiyun 546*4882a593Smuzhiyun if start < skip < end: 547*4882a593Smuzhiyun start = skip 548*4882a593Smuzhiyun 549*4882a593Smuzhiyun fmap._f_image.seek(start, os.SEEK_SET) 550*4882a593Smuzhiyun 551*4882a593Smuzhiyun written += start - skip - written 552*4882a593Smuzhiyun if length and written >= length: 553*4882a593Smuzhiyun dst_file.seek(seek + length, os.SEEK_SET) 554*4882a593Smuzhiyun dst_file.close() 555*4882a593Smuzhiyun return 556*4882a593Smuzhiyun 557*4882a593Smuzhiyun dst_file.seek(seek + start - skip, os.SEEK_SET) 558*4882a593Smuzhiyun 559*4882a593Smuzhiyun chunk_size = 1024 * 1024 560*4882a593Smuzhiyun to_read = end - start 561*4882a593Smuzhiyun read = 0 562*4882a593Smuzhiyun 563*4882a593Smuzhiyun while read < to_read: 564*4882a593Smuzhiyun if read + chunk_size > to_read: 565*4882a593Smuzhiyun chunk_size = to_read - read 566*4882a593Smuzhiyun size = chunk_size 567*4882a593Smuzhiyun if length and written + size > length: 568*4882a593Smuzhiyun size = length - written 569*4882a593Smuzhiyun chunk = fmap._f_image.read(size) 570*4882a593Smuzhiyun dst_file.write(chunk) 571*4882a593Smuzhiyun read += size 572*4882a593Smuzhiyun written += size 573*4882a593Smuzhiyun if written == length: 574*4882a593Smuzhiyun dst_file.close() 575*4882a593Smuzhiyun return 576*4882a593Smuzhiyun dst_file.close() 577