xref: /OK3568_Linux_fs/yocto/poky/scripts/lib/wic/filemap.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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