1# filelike/wrappers/fixedblocksize.py
2#
3# Copyright (C) 2006-2008, Ryan Kelly
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20"""
21
22    filelike.wrappers.fixedblocksize:  read/write only on block boundaries
23
24This module provides the filelike wrapper 'FixedBlockSize' to ensure that
25reads/writes to the underlying file are only performed at block boundaries.
26
27"""
28
29import filelike
30from filelike.wrappers import FileWrapper
31
32
33class FixedBlockSize(FileWrapper):
34    """Class reading/writing to files at a fixed block size.
35
36    This file wrapper can be used to read or write to a file-like
37    object at a specific block size.  All reads request strings
38    whose length is a multiple of the block size, and all writes
39    pass on strings of a similar nature.  This could be useful, for
40    example, to write data to a cipher function without manually
41    chunking text to match the cipher's block size.
42
43    No padding is added to the file if its length is not a multiple
44    of the blocksize.  This might cause things to fail when this file
45    is flushed or closed, since an incorrectly-sized string could be
46    given in this case.
47    """
48
49    def __init__(self,fileobj,blocksize,mode=None):
50        self.blocksize = blocksize
51        super(FixedBlockSize,self).__init__(fileobj,mode)
52
53    def _round_up(self,num):
54        """Round <num> up to a multiple of the block size."""
55        if num % self.blocksize == 0:
56            return num
57        return ((num/self.blocksize)+1) * self.blocksize
58
59    def _round_down(self,num):
60        """Round <num> down to a multiple of the block size."""
61        if num % self.blocksize == 0:
62            return num
63        return (num/self.blocksize) * self.blocksize
64
65    def _read(self,sizehint=-1):
66        """Read approximately <sizehint> bytes from the file."""
67        if sizehint >= 0:
68            sizehint = self._round_up(sizehint)
69        data = self._fileobj.read(sizehint)
70        if data == "":
71            return None
72        return data
73
74    def _write(self,data,flushing=False):
75        """Write the given string to the file.
76
77        When flushing data to the file, it may need to be padded to the
78        block size.  We attempt to read additional data from the
79        underlying file to use for the padding.
80        """
81        size = self._round_down(len(data))
82        self._fileobj.write(data[:size])
83        if len(data) == size:
84            return ""
85        if not flushing:
86            return data[size:]
87        # Flushing, so we need to try to pad the data with existing contents.
88        # If we can't find such contents, just write at non-blocksize.
89        if self._check_mode("r"):
90            nextBlock = self._fileobj.read(self.blocksize)
91            self._fileobj.seek(-1*len(nextBlock),1)
92        else:
93            nextBlock = ""
94        padstart = len(data) - size
95        self._fileobj.write(data[size:] + nextBlock[padstart:])
96        # Seek back to start of previous block, if the file is readable.
97        if self._check_mode("r"):
98            self.seek(padstart - self.blocksize,1)
99        return ""
100
101    # TODO: primitive implementation of relative seek
102    def _seek(self,offset,whence):
103        """Absolute seek, repecting block boundaries.
104
105        This method performs an absolute file seek to the block boundary
106        closest to (but not exceeding) the specified offset.
107        """
108        if whence != 0:
109            raise NotImplementedError
110        boundary = self._round_down(offset)
111        self._fileobj.seek(boundary,0)
112        if boundary == offset:
113            return ""
114        else:
115            data = self._fileobj.read(self.blocksize)
116            diff = offset - boundary - len(data)
117            if diff > 0:
118                # Seeked past end of file.  Actually do this on fileobj, so
119                # that it will raise an error if appropriate.
120                self._fileobj.seek(diff,1)
121                self._fileobj.seek(-1*diff,1)
122            self._fileobj.seek(-1*len(data),1)
123            return data[:(offset-boundary)]
124
125
126