mirror of
https://git.yoctoproject.org/poky
synced 2026-06-12 13:53:48 +02:00
While working on splitting-out wic from oe-core, on my openSUSE Leap
16.0 machine, the moment I split wic out, 2 oe-selftests always failed
with 100% reproducibility:
- wic.ModifyTests.test_wic_cp_ext
- wic.Wic2.test_expand_mbr_image
In both cases the symptom is the same: the filesystem has inode tables
that are completely zeroed out. Both issues are linked together to the
same underlying fault.
FilemapSeek._get_ranges() is a generator. Due to the nature of finding
each hole/data extent one at a time using the lseek() system call,
it calls os.lseek() on a raw file descriptor, then yields, then the
caller, sparse_copy(), calls file.seek() + file.read() on a Python
BufferedReader wrapping that same fd — then the generator resumes and
calls os.lseek() again. This interleaving of raw os.lseek() and buffered
I/O on the same fd is undefined behaviour from Python's perspective.
The BufferedReader tracks its own idea of the fd's position and buffer
contents; os.lseek() changes the position behind its back. This can
corrupt its internal state and cause read() to return stale/zero data.
This code, however, has existed in wic since it was written, so why
was it not noticed before? It turns out this bug was being masked by a
number of implementation details that changed, especially when wic was
split out for oe-core. These changes conspired together to cause the bug
to be triggered.
One of the root causes of this bug is that Python 3.14 increased the
default buffer size from 8KB to 128KB[1]. With 8 KB buffers, read()s
either go through the direct-read path leaving the buffer empty, or
if it fills in 8KB chunks the buffer is fully drained. Either way,
with a small buffer, read()s do a real raw seek. No fast path. No
corruption. With a 128KB buffer, however, a much larger window exists
where BufferedReader.seek() can take the fast-path after the raw file
descriptor has already been repositioned by os.lseek() in the generator.
With the smaller buffer, this window was too narrow to hit in practice.
This is fixed by opening a second file object in FilemapSeek.__init__()
dedicated to SEEK_DATA/SEEK_HOLE probes, leaving the data-reading handle
(self._f_image) untouched.
This explains why the corruption is deterministic and tied to specific
block boundaries, why it only manifests with the split-out version using
Python 3.14 (on systems that are using Python versions less than 3.14 on
the host), and why using a separate file descriptor for reading bypasses
the issue entirely.
This is not an intermittent bug. For a more detailed explanation
including log files, in-depth analysis, and a standalone Python
reproducer, please see the linked bugzilla entry.
Fixes: [YOCTO #16197]
[1] b1b4f9625c
b1b4f9625c5f ("gh-117151: IO performance improvement, increase io.DEFAULT_BUFFER_SIZE to 128k (GH-118144)")
AI-Generated: codex/claude-opus-4.6 (xhigh)
(From OE-Core rev: 37a45219dd204b07bad40576fefccb2cf85b255c)
Signed-off-by: Trevor Woerner <twoerner@gmail.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
(cherry picked from commit 481969844385f2fa40a1230ca50253ec4ff516cd)
Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
Signed-off-by: Paul Barker <paul@pbarker.dev>
591 lines
21 KiB
Python
591 lines
21 KiB
Python
#
|
|
# Copyright (c) 2012 Intel, Inc.
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
#
|
|
|
|
"""
|
|
This module implements python implements a way to get file block. Two methods
|
|
are supported - the FIEMAP ioctl and the 'SEEK_HOLE / SEEK_DATA' features of
|
|
the file seek syscall. The former is implemented by the 'FilemapFiemap' class,
|
|
the latter is implemented by the 'FilemapSeek' class. Both classes provide the
|
|
same API. The 'filemap' function automatically selects which class can be used
|
|
and returns an instance of the class.
|
|
"""
|
|
|
|
# Disable the following pylint recommendations:
|
|
# * Too many instance attributes (R0902)
|
|
# pylint: disable=R0902
|
|
|
|
import errno
|
|
import os
|
|
import struct
|
|
import array
|
|
import fcntl
|
|
import tempfile
|
|
import logging
|
|
|
|
def get_block_size(file_obj):
|
|
"""
|
|
Returns block size for file object 'file_obj'. Errors are indicated by the
|
|
'IOError' exception.
|
|
"""
|
|
# Get the block size of the host file-system for the image file by calling
|
|
# the FIGETBSZ ioctl (number 2).
|
|
try:
|
|
binary_data = fcntl.ioctl(file_obj, 2, struct.pack('I', 0))
|
|
bsize = struct.unpack('I', binary_data)[0]
|
|
except OSError:
|
|
bsize = None
|
|
|
|
# If ioctl causes OSError or give bsize to zero failback to os.fstat
|
|
if not bsize:
|
|
import os
|
|
stat = os.fstat(file_obj.fileno())
|
|
if hasattr(stat, 'st_blksize'):
|
|
bsize = stat.st_blksize
|
|
else:
|
|
raise IOError("Unable to determine block size")
|
|
|
|
# The logic in this script only supports a maximum of a 4KB
|
|
# block size
|
|
max_block_size = 4 * 1024
|
|
if bsize > max_block_size:
|
|
bsize = max_block_size
|
|
|
|
return bsize
|
|
|
|
class ErrorNotSupp(Exception):
|
|
"""
|
|
An exception of this type is raised when the 'FIEMAP' or 'SEEK_HOLE' feature
|
|
is not supported either by the kernel or the file-system.
|
|
"""
|
|
pass
|
|
|
|
class Error(Exception):
|
|
"""A class for all the other exceptions raised by this module."""
|
|
pass
|
|
|
|
|
|
class _FilemapBase(object):
|
|
"""
|
|
This is a base class for a couple of other classes in this module. This
|
|
class simply performs the common parts of the initialization process: opens
|
|
the image file, gets its size, etc. The 'log' parameter is the logger object
|
|
to use for printing messages.
|
|
"""
|
|
|
|
def __init__(self, image, log=None):
|
|
"""
|
|
Initialize a class instance. The 'image' argument is full path to the
|
|
file or file object to operate on.
|
|
"""
|
|
|
|
self._log = log
|
|
if self._log is None:
|
|
self._log = logging.getLogger(__name__)
|
|
|
|
self._f_image_needs_close = False
|
|
|
|
if hasattr(image, "fileno"):
|
|
self._f_image = image
|
|
self._image_path = image.name
|
|
else:
|
|
self._image_path = image
|
|
self._open_image_file()
|
|
|
|
try:
|
|
self.image_size = os.fstat(self._f_image.fileno()).st_size
|
|
except IOError as err:
|
|
raise Error("cannot get information about file '%s': %s"
|
|
% (self._f_image.name, err))
|
|
|
|
try:
|
|
self.block_size = get_block_size(self._f_image)
|
|
except IOError as err:
|
|
raise Error("cannot get block size for '%s': %s"
|
|
% (self._image_path, err))
|
|
|
|
self.blocks_cnt = self.image_size + self.block_size - 1
|
|
self.blocks_cnt //= self.block_size
|
|
|
|
try:
|
|
self._f_image.flush()
|
|
except IOError as err:
|
|
raise Error("cannot flush image file '%s': %s"
|
|
% (self._image_path, err))
|
|
|
|
try:
|
|
os.fsync(self._f_image.fileno()),
|
|
except OSError as err:
|
|
raise Error("cannot synchronize image file '%s': %s "
|
|
% (self._image_path, err.strerror))
|
|
|
|
self._log.debug("opened image \"%s\"" % self._image_path)
|
|
self._log.debug("block size %d, blocks count %d, image size %d"
|
|
% (self.block_size, self.blocks_cnt, self.image_size))
|
|
|
|
def __del__(self):
|
|
"""The class destructor which just closes the image file."""
|
|
if self._f_image_needs_close:
|
|
self._f_image.close()
|
|
|
|
def _open_image_file(self):
|
|
"""Open the image file."""
|
|
try:
|
|
self._f_image = open(self._image_path, 'rb')
|
|
except IOError as err:
|
|
raise Error("cannot open image file '%s': %s"
|
|
% (self._image_path, err))
|
|
|
|
self._f_image_needs_close = True
|
|
|
|
def block_is_mapped(self, block): # pylint: disable=W0613,R0201
|
|
"""
|
|
This method has has to be implemented by child classes. It returns
|
|
'True' if block number 'block' of the image file is mapped and 'False'
|
|
otherwise.
|
|
"""
|
|
|
|
raise Error("the method is not implemented")
|
|
|
|
def get_mapped_ranges(self, start, count): # pylint: disable=W0613,R0201
|
|
"""
|
|
This method has has to be implemented by child classes. This is a
|
|
generator which yields ranges of mapped blocks in the file. The ranges
|
|
are tuples of 2 elements: [first, last], where 'first' is the first
|
|
mapped block and 'last' is the last mapped block.
|
|
|
|
The ranges are yielded for the area of the file of size 'count' blocks,
|
|
starting from block 'start'.
|
|
"""
|
|
|
|
raise Error("the method is not implemented")
|
|
|
|
|
|
# The 'SEEK_HOLE' and 'SEEK_DATA' options of the file seek system call
|
|
_SEEK_DATA = 3
|
|
_SEEK_HOLE = 4
|
|
|
|
def _lseek(file_obj, offset, whence):
|
|
"""This is a helper function which invokes 'os.lseek' for file object
|
|
'file_obj' and with specified 'offset' and 'whence'. The 'whence'
|
|
argument is supposed to be either '_SEEK_DATA' or '_SEEK_HOLE'. When
|
|
there is no more data or hole starting from 'offset', this function
|
|
returns '-1'. Otherwise the data or hole position is returned."""
|
|
|
|
try:
|
|
return os.lseek(file_obj.fileno(), offset, whence)
|
|
except OSError as err:
|
|
# The 'lseek' system call returns the ENXIO if there is no data or
|
|
# hole starting from the specified offset.
|
|
if err.errno == errno.ENXIO:
|
|
return -1
|
|
elif err.errno == errno.EINVAL:
|
|
raise ErrorNotSupp("the kernel or file-system does not support "
|
|
"\"SEEK_HOLE\" and \"SEEK_DATA\"")
|
|
else:
|
|
raise
|
|
|
|
class FilemapSeek(_FilemapBase):
|
|
"""
|
|
This class uses the 'SEEK_HOLE' and 'SEEK_DATA' to find file block mapping.
|
|
Unfortunately, the current implementation requires the caller to have write
|
|
access to the image file.
|
|
"""
|
|
|
|
def __init__(self, image, log=None):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
|
|
# Call the base class constructor first
|
|
_FilemapBase.__init__(self, image, log)
|
|
self._log.debug("FilemapSeek: initializing")
|
|
|
|
# Open a separate file handle for SEEK_DATA/SEEK_HOLE probes so
|
|
# that the lseek() calls do not disturb the BufferedReader state
|
|
# of self._f_image, which sparse_copy() uses for data reading.
|
|
# Sharing a single fd between os.lseek() and buffered read()
|
|
# has the potential to cause data corruption.
|
|
self._f_seek = open(self._image_path, 'rb')
|
|
|
|
self._probe_seek_hole()
|
|
|
|
def _probe_seek_hole(self):
|
|
"""
|
|
Check whether the system implements 'SEEK_HOLE' and 'SEEK_DATA'.
|
|
Unfortunately, there seems to be no clean way for detecting this,
|
|
because often the system just fakes them by just assuming that all
|
|
files are fully mapped, so 'SEEK_HOLE' always returns EOF and
|
|
'SEEK_DATA' always returns the requested offset.
|
|
|
|
I could not invent a better way of detecting the fake 'SEEK_HOLE'
|
|
implementation than just to create a temporary file in the same
|
|
directory where the image file resides. It would be nice to change this
|
|
to something better.
|
|
"""
|
|
|
|
directory = os.path.dirname(self._image_path)
|
|
|
|
try:
|
|
tmp_obj = tempfile.TemporaryFile("w+", dir=directory)
|
|
except IOError as err:
|
|
raise ErrorNotSupp("cannot create a temporary in \"%s\": %s" \
|
|
% (directory, err))
|
|
|
|
try:
|
|
os.ftruncate(tmp_obj.fileno(), self.block_size)
|
|
except OSError as err:
|
|
raise ErrorNotSupp("cannot truncate temporary file in \"%s\": %s"
|
|
% (directory, err))
|
|
|
|
offs = _lseek(tmp_obj, 0, _SEEK_HOLE)
|
|
if offs != 0:
|
|
# We are dealing with the stub 'SEEK_HOLE' implementation which
|
|
# always returns EOF.
|
|
self._log.debug("lseek(0, SEEK_HOLE) returned %d" % offs)
|
|
raise ErrorNotSupp("the file-system does not support "
|
|
"\"SEEK_HOLE\" and \"SEEK_DATA\" but only "
|
|
"provides a stub implementation")
|
|
|
|
tmp_obj.close()
|
|
|
|
def block_is_mapped(self, block):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
offs = _lseek(self._f_seek, block * self.block_size, _SEEK_DATA)
|
|
if offs == -1:
|
|
result = False
|
|
else:
|
|
result = (offs // self.block_size == block)
|
|
|
|
self._log.debug("FilemapSeek: block_is_mapped(%d) returns %s"
|
|
% (block, result))
|
|
return result
|
|
|
|
def _get_ranges(self, start, count, whence1, whence2):
|
|
"""
|
|
This function implements 'get_mapped_ranges()' depending
|
|
on what is passed in the 'whence1' and 'whence2' arguments.
|
|
"""
|
|
|
|
assert whence1 != whence2
|
|
end = start * self.block_size
|
|
limit = end + count * self.block_size
|
|
|
|
while True:
|
|
start = _lseek(self._f_seek, end, whence1)
|
|
if start == -1 or start >= limit or start == self.image_size:
|
|
break
|
|
|
|
end = _lseek(self._f_seek, start, whence2)
|
|
if end == -1 or end == self.image_size:
|
|
end = self.blocks_cnt * self.block_size
|
|
if end > limit:
|
|
end = limit
|
|
|
|
start_blk = start // self.block_size
|
|
end_blk = end // self.block_size - 1
|
|
self._log.debug("FilemapSeek: yielding range (%d, %d)"
|
|
% (start_blk, end_blk))
|
|
yield (start_blk, end_blk)
|
|
|
|
def get_mapped_ranges(self, start, count):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
self._log.debug("FilemapSeek: get_mapped_ranges(%d, %d(%d))"
|
|
% (start, count, start + count - 1))
|
|
return self._get_ranges(start, count, _SEEK_DATA, _SEEK_HOLE)
|
|
|
|
|
|
# Below goes the FIEMAP ioctl implementation, which is not very readable
|
|
# because it deals with the rather complex FIEMAP ioctl. To understand the
|
|
# code, you need to know the FIEMAP interface, which is documented in the
|
|
# "Documentation/filesystems/fiemap.txt" file in the Linux kernel sources.
|
|
|
|
# Format string for 'struct fiemap'
|
|
_FIEMAP_FORMAT = "=QQLLLL"
|
|
# sizeof(struct fiemap)
|
|
_FIEMAP_SIZE = struct.calcsize(_FIEMAP_FORMAT)
|
|
# Format string for 'struct fiemap_extent'
|
|
_FIEMAP_EXTENT_FORMAT = "=QQQQQLLLL"
|
|
# sizeof(struct fiemap_extent)
|
|
_FIEMAP_EXTENT_SIZE = struct.calcsize(_FIEMAP_EXTENT_FORMAT)
|
|
# The FIEMAP ioctl number
|
|
_FIEMAP_IOCTL = 0xC020660B
|
|
# This FIEMAP ioctl flag which instructs the kernel to sync the file before
|
|
# reading the block map
|
|
_FIEMAP_FLAG_SYNC = 0x00000001
|
|
# Size of the buffer for 'struct fiemap_extent' elements which will be used
|
|
# when invoking the FIEMAP ioctl. The larger is the buffer, the less times the
|
|
# FIEMAP ioctl will be invoked.
|
|
_FIEMAP_BUFFER_SIZE = 256 * 1024
|
|
|
|
class FilemapFiemap(_FilemapBase):
|
|
"""
|
|
This class provides API to the FIEMAP ioctl. Namely, it allows to iterate
|
|
over all mapped blocks and over all holes.
|
|
|
|
This class synchronizes the image file every time it invokes the FIEMAP
|
|
ioctl in order to work-around early FIEMAP implementation kernel bugs.
|
|
"""
|
|
|
|
def __init__(self, image, log=None):
|
|
"""
|
|
Initialize a class instance. The 'image' argument is full the file
|
|
object to operate on.
|
|
"""
|
|
|
|
# Call the base class constructor first
|
|
_FilemapBase.__init__(self, image, log)
|
|
self._log.debug("FilemapFiemap: initializing")
|
|
|
|
self._buf_size = _FIEMAP_BUFFER_SIZE
|
|
|
|
# Calculate how many 'struct fiemap_extent' elements fit the buffer
|
|
self._buf_size -= _FIEMAP_SIZE
|
|
self._fiemap_extent_cnt = self._buf_size // _FIEMAP_EXTENT_SIZE
|
|
assert self._fiemap_extent_cnt > 0
|
|
self._buf_size = self._fiemap_extent_cnt * _FIEMAP_EXTENT_SIZE
|
|
self._buf_size += _FIEMAP_SIZE
|
|
|
|
# Allocate a mutable buffer for the FIEMAP ioctl
|
|
self._buf = array.array('B', [0] * self._buf_size)
|
|
|
|
# Check if the FIEMAP ioctl is supported
|
|
self.block_is_mapped(0)
|
|
|
|
def _invoke_fiemap(self, block, count):
|
|
"""
|
|
Invoke the FIEMAP ioctl for 'count' blocks of the file starting from
|
|
block number 'block'.
|
|
|
|
The full result of the operation is stored in 'self._buf' on exit.
|
|
Returns the unpacked 'struct fiemap' data structure in form of a python
|
|
list (just like 'struct.upack()').
|
|
"""
|
|
|
|
if self.blocks_cnt != 0 and (block < 0 or block >= self.blocks_cnt):
|
|
raise Error("bad block number %d, should be within [0, %d]"
|
|
% (block, self.blocks_cnt))
|
|
|
|
# Initialize the 'struct fiemap' part of the buffer. We use the
|
|
# '_FIEMAP_FLAG_SYNC' flag in order to make sure the file is
|
|
# synchronized. The reason for this is that early FIEMAP
|
|
# implementations had many bugs related to cached dirty data, and
|
|
# synchronizing the file is a necessary work-around.
|
|
struct.pack_into(_FIEMAP_FORMAT, self._buf, 0, block * self.block_size,
|
|
count * self.block_size, _FIEMAP_FLAG_SYNC, 0,
|
|
self._fiemap_extent_cnt, 0)
|
|
|
|
try:
|
|
fcntl.ioctl(self._f_image, _FIEMAP_IOCTL, self._buf, 1)
|
|
except IOError as err:
|
|
# Note, the FIEMAP ioctl is supported by the Linux kernel starting
|
|
# from version 2.6.28 (year 2008).
|
|
if err.errno == errno.EOPNOTSUPP:
|
|
errstr = "FilemapFiemap: the FIEMAP ioctl is not supported " \
|
|
"by the file-system"
|
|
self._log.debug(errstr)
|
|
raise ErrorNotSupp(errstr)
|
|
if err.errno == errno.ENOTTY:
|
|
errstr = "FilemapFiemap: the FIEMAP ioctl is not supported " \
|
|
"by the kernel"
|
|
self._log.debug(errstr)
|
|
raise ErrorNotSupp(errstr)
|
|
raise Error("the FIEMAP ioctl failed for '%s': %s"
|
|
% (self._image_path, err))
|
|
|
|
return struct.unpack(_FIEMAP_FORMAT, self._buf[:_FIEMAP_SIZE])
|
|
|
|
def block_is_mapped(self, block):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
struct_fiemap = self._invoke_fiemap(block, 1)
|
|
|
|
# The 3rd element of 'struct_fiemap' is the 'fm_mapped_extents' field.
|
|
# If it contains zero, the block is not mapped, otherwise it is
|
|
# mapped.
|
|
result = bool(struct_fiemap[3])
|
|
self._log.debug("FilemapFiemap: block_is_mapped(%d) returns %s"
|
|
% (block, result))
|
|
return result
|
|
|
|
def _unpack_fiemap_extent(self, index):
|
|
"""
|
|
Unpack a 'struct fiemap_extent' structure object number 'index' from
|
|
the internal 'self._buf' buffer.
|
|
"""
|
|
|
|
offset = _FIEMAP_SIZE + _FIEMAP_EXTENT_SIZE * index
|
|
return struct.unpack(_FIEMAP_EXTENT_FORMAT,
|
|
self._buf[offset : offset + _FIEMAP_EXTENT_SIZE])
|
|
|
|
def _do_get_mapped_ranges(self, start, count):
|
|
"""
|
|
Implements most the functionality for the 'get_mapped_ranges()'
|
|
generator: invokes the FIEMAP ioctl, walks through the mapped extents
|
|
and yields mapped block ranges. However, the ranges may be consecutive
|
|
(e.g., (1, 100), (100, 200)) and 'get_mapped_ranges()' simply merges
|
|
them.
|
|
"""
|
|
|
|
block = start
|
|
while block < start + count:
|
|
struct_fiemap = self._invoke_fiemap(block, count)
|
|
|
|
mapped_extents = struct_fiemap[3]
|
|
if mapped_extents == 0:
|
|
# No more mapped blocks
|
|
return
|
|
|
|
extent = 0
|
|
while extent < mapped_extents:
|
|
fiemap_extent = self._unpack_fiemap_extent(extent)
|
|
|
|
# Start of the extent
|
|
extent_start = fiemap_extent[0]
|
|
# Starting block number of the extent
|
|
extent_block = extent_start // self.block_size
|
|
# Length of the extent
|
|
extent_len = fiemap_extent[2]
|
|
# Count of blocks in the extent
|
|
extent_count = extent_len // self.block_size
|
|
|
|
# Extent length and offset have to be block-aligned
|
|
assert extent_start % self.block_size == 0
|
|
assert extent_len % self.block_size == 0
|
|
|
|
if extent_block > start + count - 1:
|
|
return
|
|
|
|
first = max(extent_block, block)
|
|
last = min(extent_block + extent_count, start + count) - 1
|
|
yield (first, last)
|
|
|
|
extent += 1
|
|
|
|
block = extent_block + extent_count
|
|
|
|
def get_mapped_ranges(self, start, count):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
self._log.debug("FilemapFiemap: get_mapped_ranges(%d, %d(%d))"
|
|
% (start, count, start + count - 1))
|
|
iterator = self._do_get_mapped_ranges(start, count)
|
|
first_prev, last_prev = next(iterator)
|
|
|
|
for first, last in iterator:
|
|
if last_prev == first - 1:
|
|
last_prev = last
|
|
else:
|
|
self._log.debug("FilemapFiemap: yielding range (%d, %d)"
|
|
% (first_prev, last_prev))
|
|
yield (first_prev, last_prev)
|
|
first_prev, last_prev = first, last
|
|
|
|
self._log.debug("FilemapFiemap: yielding range (%d, %d)"
|
|
% (first_prev, last_prev))
|
|
yield (first_prev, last_prev)
|
|
|
|
class FilemapNobmap(_FilemapBase):
|
|
"""
|
|
This class is used when both the 'SEEK_DATA/HOLE' and FIEMAP are not
|
|
supported by the filesystem or kernel.
|
|
"""
|
|
|
|
def __init__(self, image, log=None):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
|
|
# Call the base class constructor first
|
|
_FilemapBase.__init__(self, image, log)
|
|
self._log.debug("FilemapNobmap: initializing")
|
|
|
|
def block_is_mapped(self, block):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
return True
|
|
|
|
def get_mapped_ranges(self, start, count):
|
|
"""Refer the '_FilemapBase' class for the documentation."""
|
|
self._log.debug("FilemapNobmap: get_mapped_ranges(%d, %d(%d))"
|
|
% (start, count, start + count - 1))
|
|
yield (start, start + count -1)
|
|
|
|
def filemap(image, log=None):
|
|
"""
|
|
Create and return an instance of a Filemap class - 'FilemapFiemap' or
|
|
'FilemapSeek', depending on what the system we run on supports. If the
|
|
FIEMAP ioctl is supported, an instance of the 'FilemapFiemap' class is
|
|
returned. Otherwise, if 'SEEK_HOLE' is supported an instance of the
|
|
'FilemapSeek' class is returned. If none of these are supported, the
|
|
function generates an 'Error' type exception.
|
|
"""
|
|
|
|
try:
|
|
return FilemapFiemap(image, log)
|
|
except ErrorNotSupp:
|
|
try:
|
|
return FilemapSeek(image, log)
|
|
except ErrorNotSupp:
|
|
return FilemapNobmap(image, log)
|
|
|
|
def sparse_copy(src_fname, dst_fname, skip=0, seek=0,
|
|
length=0, api=None):
|
|
"""
|
|
Efficiently copy sparse file to or into another file.
|
|
|
|
src_fname: path to source file
|
|
dst_fname: path to destination file
|
|
skip: skip N bytes at thestart of src
|
|
seek: seek N bytes from the start of dst
|
|
length: read N bytes from src and write them to dst
|
|
api: FilemapFiemap or FilemapSeek object
|
|
"""
|
|
if not api:
|
|
api = filemap
|
|
fmap = api(src_fname)
|
|
try:
|
|
dst_file = open(dst_fname, 'r+b')
|
|
except IOError:
|
|
dst_file = open(dst_fname, 'wb')
|
|
if length:
|
|
dst_size = length + seek
|
|
else:
|
|
dst_size = os.path.getsize(src_fname) + seek - skip
|
|
dst_file.truncate(dst_size)
|
|
|
|
written = 0
|
|
for first, last in fmap.get_mapped_ranges(0, fmap.blocks_cnt):
|
|
start = first * fmap.block_size
|
|
end = (last + 1) * fmap.block_size
|
|
|
|
if skip >= end:
|
|
continue
|
|
|
|
if start < skip < end:
|
|
start = skip
|
|
|
|
fmap._f_image.seek(start, os.SEEK_SET)
|
|
|
|
written += start - skip - written
|
|
if length and written >= length:
|
|
dst_file.seek(seek + length, os.SEEK_SET)
|
|
dst_file.close()
|
|
return
|
|
|
|
dst_file.seek(seek + start - skip, os.SEEK_SET)
|
|
|
|
chunk_size = 1024 * 1024
|
|
to_read = end - start
|
|
read = 0
|
|
|
|
while read < to_read:
|
|
if read + chunk_size > to_read:
|
|
chunk_size = to_read - read
|
|
size = chunk_size
|
|
if length and written + size > length:
|
|
size = length - written
|
|
chunk = fmap._f_image.read(size)
|
|
dst_file.write(chunk)
|
|
read += size
|
|
written += size
|
|
if written == length:
|
|
dst_file.close()
|
|
return
|
|
dst_file.close()
|