view mercurial/state.py @ 40626:87a872555e90

revlog: detect incomplete revlog reads _readsegment() is supposed to return N bytes of revlog revision data starting at a file offset. Surprisingly, its behavior before this patch never verified that it actually read and returned N bytes! Instead, it would perform the read(), then return whatever data was available. And even more surprisingly, nothing in the call chain appears to have been validating that it received all the data it was expecting. This behavior could lead to partial or incomplete revision chunks being operated on. This could result in e.g. cached deltas being applied against incomplete base revisions. The delta application process would happily perform this operation. Only hash verification would detect the corruption and save us. This commit changes the behavior of raw revlog reading to validate that we actually read() the number of bytes that were requested. We will raise a more specific error faster, rather than possibly have it go undetected or manifest later in the call stack, at delta application or hash verification. Differential Revision: https://phab.mercurial-scm.org/D5266
author Gregory Szorc <gregory.szorc@gmail.com>
date Tue, 13 Nov 2018 12:30:59 -0800
parents 5bfab9400daf
children 050ea8eb42a5
line wrap: on
line source

# state.py - writing and reading state files in Mercurial
#
# Copyright 2018 Pulkit Goyal <pulkitmgoyal@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

"""
This file contains class to wrap the state for commands and other
related logic.

All the data related to the command state is stored as dictionary in the object.
The class has methods using which the data can be stored to disk in a file under
.hg/ directory.

We store the data on disk in cbor, for which we use the third party cbor library
to serialize and deserialize data.
"""

from __future__ import absolute_import

from . import (
    error,
    util,
)
from .utils import (
    cborutil,
)

class cmdstate(object):
    """a wrapper class to store the state of commands like `rebase`, `graft`,
    `histedit`, `shelve` etc. Extensions can also use this to write state files.

    All the data for the state is stored in the form of key-value pairs in a
    dictionary.

    The class object can write all the data to a file in .hg/ directory and
    can populate the object data reading that file.

    Uses cbor to serialize and deserialize data while writing and reading from
    disk.
    """

    def __init__(self, repo, fname):
        """ repo is the repo object
        fname is the file name in which data should be stored in .hg directory
        """
        self._repo = repo
        self.fname = fname

    def read(self):
        """read the existing state file and return a dict of data stored"""
        return self._read()

    def save(self, version, data):
        """write all the state data stored to .hg/<filename> file

        we use third-party library cbor to serialize data to write in the file.
        """
        if not isinstance(version, int):
            raise error.ProgrammingError("version of state file should be"
                                         " an integer")

        with self._repo.vfs(self.fname, 'wb', atomictemp=True) as fp:
            fp.write('%d\n' % version)
            for chunk in cborutil.streamencode(data):
                fp.write(chunk)

    def _read(self):
        """reads the state file and returns a dictionary which contain
        data in the same format as it was before storing"""
        with self._repo.vfs(self.fname, 'rb') as fp:
            try:
                int(fp.readline())
            except ValueError:
                raise error.CorruptedState("unknown version of state file"
                                           " found")

            return cborutil.decodeall(fp.read())[0]

    def delete(self):
        """drop the state file if exists"""
        util.unlinkpath(self._repo.vfs.join(self.fname), ignoremissing=True)

    def exists(self):
        """check whether the state file exists or not"""
        return self._repo.vfs.exists(self.fname)