Kirjautuminen

Haku

Tehtävät

Projektit: FileSlice: Tiedoston osan kopiointi

Kirjoittaja: qalle

Kirjoitettu: 23.08.2015 – 01.10.2017

Tagit: hyvää koodia, projektin esittely, vinkki, komentorivi

Apuohjelma, joka kopioi halutun osan (tietyn määrän tavuja tietystä osoitteesta alkaen) binääritiedostosta toiseen binääritiedostoon. Ohjelma on tarkoitettu täydentämään heksaeditoreja ja demonstroi seuraavia Pythonin ominaisuuksia:

Ohjelma

"""
FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi.
Testattu Python 3:lla Windows 10:llä.

Funktiohierarkia:
    main()
        parse_arguments()
            parse_integer_argument()
        copy_slice()
            validate_and_adjust_integer_arguments()
            to_printable()
            read_file_in_chunks()
"""

import sys
import os.path
import getopt

# kopiointipuskurin oletuskoko tavuina
FILE_COPY_BUFFER_SIZE = 2 ** 20

# lukuparametreissa sallitut loppuliitteet
NUMBER_SUFFIXES = {
    "K" : 2 ** 10,  # kilo
    "M" : 2 ** 20,  # mega
    "G" : 2 ** 30,  # giga
    "T" : 2 ** 40,  # tera
}

# pitkiä tulostettavia tekstejä (virheilmoituksia ja ohje)

ARGUMENT_ERROR = """\
Väärä määrä pakollisia komentoriviparametreja. Näet ohjeen ajamalla ohjelman
ilman parametreja.\
"""

OPTION_ERROR = """\
Virhe valinnaisissa komentoriviparametreissa. Näet ohjeen ajamalla ohjelman
ilman parametreja.\
"""

INTEGER_ERROR = """\
Epäkelpo kokonaislukuparametri. Näet ohjeen ajamalla ohjelman ilman
parametreja.\
"""

HELP_TEXT = """\
FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi

Valinnaiset komentoriviparametrit (missä järjestyksessä tahansa mutta ennen
pakollisia parametreja; kirjainkoolla ei ole väliä):

-fN
--from-address=N
    Ensimmäisen kopioitavan tavun osoite on N.
    Oletusarvo: 0
-tN
--to-address=N
    Viimeisen kopioitavan tavun osoite on N.
    Oletusarvo: -1
    Tätä parametria ei saa antaa yhdessä --length:in kanssa.
-lN
--length=N
    Kopioitavan palan pituus on N tavua (1 tai suurempi).
    Tätä parametria ei saa antaa yhdessä --to-address:in kanssa.

Pakolliset komentoriviparametrit (valinnaisten parametrien jälkeen, tässä
järjestyksessä):

Lähdetiedosto
    Tiedosto, josta kopioidaan.
Kohdetiedosto
    Tiedosto, johon kopioidaan.
    Tämä tiedosto ylikirjoitetaan, jos se on jo olemassa.

Huomautuksia:
    * Parametreissa --from-address ja --to-address ei-negatiiviset arvot
      tarkoittavat etäisyyttä tiedoston alusta: 0 = ensimmäinen tavu, 1 =
      toinen, jne. Negatiiviset arvot tarkoittavat etäisyyttä tiedoston
      lopusta: -1 = viimeinen tavu, -2 = toiseksi viimeinen, jne.
    * Oletuksena lähdetiedosto kopioidaan kokonaan kohdetiedostoon.
    * Numeeriset parametrit (--from-address, --to-address ja --length) voidaan
      antaa myös 16-kantaisina: esim. 0xff = 255 ja -0xff = -255.
    * Numeeristen parametrien lopussa saa olla jokin seuraavista liitteistä:
          K: kilo (2 ** 10)
          M: mega (2 ** 20)
          G: giga (2 ** 30)
          T: tera (2 ** 40)\
"""

def parse_integer_argument(options, shortName, longName):
    """Tulkitse kokonaislukumuotoinen valinnainen komentoriviparametri, esim.
    "-0x10M". Palauta None, jos parametria ei ole annettu."""

    # lue parametri merkkijonona
    string = options.get(shortName, options.get(longName))
    if string is None:
        return None

    # muunna parametrin kirjaimet isoiksi, jotta jälkiliite voidaan tunnistaa
    string = string.upper()

    # jos syötteessä on jälkiliite, poista se ja muista kerroin
    if len(string) > 0 and string[-1] in NUMBER_SUFFIXES:
        multiplier = NUMBER_SUFFIXES[string[-1]]
        string = string[:-1]
    else:
        multiplier = 1

    # tulkitse jäljelle jäänyt parametri (esim. "-0x10")
    try:
        value = int(string, 0)
    except ValueError:
        exit(INTEGER_ERROR)

    return value * multiplier

def parse_arguments():
    """Tulkitse komentoriviparametrit getopt-moduulin avulla ja palauta ne
    dict:issä. (Asetuksia ei vielä juurikaan tarkisteta, koska luettavan
    tiedoston kokoa ei tiedetä.)"""

    # options = valinnaiset (viiva- ja kaksoisviiva-alkuiset), arguments = muut
    try:
        (options, arguments) = getopt.getopt(
            sys.argv[1:],
            "f:t:l:",
            ["from-address=", "to-address=", "length="]
        )
    except getopt.GetoptError:
        exit(OPTION_ERROR)

    # tarkista pakollisten parametrien määrä
    if len(arguments) != 2:
        exit(ARGUMENT_ERROR)

    # muunna valinnaiset parametrit dict-tyyppiseksi käsittelyn helpottamiseksi
    options = dict(options)

    return {
        # kopioinnin aloitusosoite (int/None)
        "fromAddress": parse_integer_argument(options, "-f", "--from-address"),
        # kopioinnin lopetusosoite (int/None)
        "toAddress": parse_integer_argument(options, "-t", "--to-address"),
        # kopioitavan osan pituus (int/None)
        "length": parse_integer_argument(options, "-l", "--length"),
        # lähdetiedosto (str)
        "sourceFile": arguments[0],
        # kohdetiedosto (str)
        "targetFile": arguments[1],
    }

def validate_and_adjust_integer_arguments(settings, sourceSize):
    """Tarkista ja muunna kokonaislukumuotoiset asetukset nyt, kun
    lähdetiedoston koko tiedetään."""

    # kopioitavan palan alkuosoite (aseta oletusarvoon tai muunna tiedoston
    # alusta lasketuksi)
    if settings["fromAddress"] is None:
        settings["fromAddress"] = 0
    else:
        if settings["fromAddress"] < 0:
            settings["fromAddress"] += sourceSize
        if not 0 <= settings["fromAddress"] <= sourceSize - 1:
            exit("Kopioitavan palan alkuosoite on liian pieni tai suuri.")

    # kopioitavan palan loppuosoite ja pituus
    if settings["toAddress"] is None and settings["length"] is None:
        # kumpaakaan ei annettu -> pituus oletusarvoon
        settings["length"] = sourceSize - settings["fromAddress"]
    elif settings["toAddress"] is None:
        # vain pituus annettu -> tarkista
        maxLength = sourceSize - settings["fromAddress"]
        if not 1 <= settings["length"] <= maxLength:
            exit("Kopioitavan palan pituus on liian pieni tai suuri.")
    elif settings["length"] is None:
        # vain loppuosoite annettu -> muunna tiedoston alusta lasketuksi,
        # tarkista ja muunna pituudeksi
        if settings["toAddress"] < 0:
            settings["toAddress"] += sourceSize
        minToAddress = settings["fromAddress"]
        maxToAddress = sourceSize - 1
        if not minToAddress <= settings["toAddress"] <= maxToAddress:
            exit("Kopioitavan palan loppuosoite on liian pieni tai suuri.")
        settings["length"] = settings["toAddress"] - settings["fromAddress"] + 1
    else:
        # molemmat annettu
        exit(OPTION_ERROR)

    # poista pituudeksi muunnettu loppuosoite tarpeettomana
    del settings["toAddress"]

    return settings

def to_printable(string):
    """Korvaa merkkijonon muut kuin 7-bittiset ASCII-merkit
    kenoviivakoodeilla."""

    byteString = string.encode("ascii", errors = "backslashreplace")
    return byteString.decode("ascii")

def read_file_in_chunks(sourceHnd, fromAddress, bytesLeft):
    """Generaattori, joka palauttaa tiedostosta halutut tavut.
    sourceHnd: lähdetiedosto
    fromAddress: ensimmäisen luettavan tavun osoite
    bytesLeft: luettavien tavujen määrä
    generoi: 1...FILE_COPY_BUFFER_SIZE luettua tavua/kutsu"""

    sourceHnd.seek(fromAddress)

    while bytesLeft > 0:
        # laske lohkon koko (pienempi jäljellä olevien tavujen määrästä ja
        # lohkon maksimikoosta)
        chunkSize = min(bytesLeft, FILE_COPY_BUFFER_SIZE)
        # lue ja palauta lohko lähdetiedostosta (koska tämä funktio on
        # generaattori, seuraavan kutsun yhteydessä jatka yield-käskyn jäljestä
        # muistaen funktion sisäinen tila)
        yield sourceHnd.read(chunkSize)
        # pienennä jäljellä olevien tavujen määrää
        bytesLeft -= chunkSize

def copy_slice(sourceHnd, targetHnd, settings):
    """Kopioi pala lähdetiedostosta kohdetiedostoksi."""

    # lue lähdetiedoston koko
    sourceSize = sourceHnd.seek(0, 2)

    if sourceSize == 0:
        exit("Virhe: lähdetiedosto on tyhjä.")

    # tarkista ja muunna kokonaislukumuotoiset asetukset
    settings = validate_and_adjust_integer_arguments(settings, sourceSize)

    toAddress = settings["fromAddress"] + settings["length"] - 1

    print('Lähdetiedosto      : "{:s}"'.format(to_printable(settings["sourceFile"])))
    print('Kohdetiedosto      : "{:s}"'.format(to_printable(settings["targetFile"])))
    print("Lähdetiedoston koko: {n:d} (0x{n:x})".format(n = sourceSize))
    print("Palan alkuosoite   : {n:d} (0x{n:x})".format(n = settings["fromAddress"]))
    print("Palan loppuosoite  : {n:d} (0x{n:x})".format(n = toAddress))
    print("Palan pituus       : {n:d} (0x{n:x})".format(n = settings["length"]))

    print("Kopioidaan...")
    targetHnd.seek(0)
    for chunk in read_file_in_chunks(sourceHnd, settings["fromAddress"], settings["length"]):
        targetHnd.write(chunk)

    # tarkista, että oikea määrä tavuja kopioitui
    targetSize = targetHnd.tell()
    if targetSize != settings["length"]:
        exit("Kopioinnissa tapahtui virhe.")

def main():
    # näytä ohjeteksti, jos ajetaan ilman komentoriviparametreja
    if len(sys.argv) == 1:
        exit(HELP_TEXT)

    # tulkitse komentoriviparametrit
    settings = parse_arguments()

    # lähde- ja kohdetiedosto eivät saa olla samat
    if os.path.abspath(settings["sourceFile"]) == os.path.abspath(settings["targetFile"]):
        exit("Lähde- ja kohdetiedosto eivät saa olla samat.")

    # kopioi pala lähdetiedostosta kohdetiedostoksi
    try:
        with open(settings["sourceFile"], "rb") as sourceHnd, \
        open(settings["targetFile"], "wb") as targetHnd:
            bytesWritten = copy_slice(sourceHnd, targetHnd, settings)
    except OSError:
        exit("Virhe luettaessa lähdetiedostoa tai kirjoitettaessa kohdetiedostoa.")

    print("Kopiointi valmis.")

if __name__ == "__main__":
    main()

Ohje suoraan ohjelmasta

FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi

Valinnaiset komentoriviparametrit (missä järjestyksessä tahansa mutta ennen
pakollisia parametreja; kirjainkoolla ei ole väliä):

-fN
--from-address=N
    Ensimmäisen kopioitavan tavun osoite on N.
    Oletusarvo: 0
-tN
--to-address=N
    Viimeisen kopioitavan tavun osoite on N.
    Oletusarvo: -1
    Tätä parametria ei saa antaa yhdessä --length:in kanssa.
-lN
--length=N
    Kopioitavan palan pituus on N tavua (1 tai suurempi).
    Tätä parametria ei saa antaa yhdessä --to-address:in kanssa.

Pakolliset komentoriviparametrit (valinnaisten parametrien jälkeen, tässä
järjestyksessä):

Lähdetiedosto
    Tiedosto, josta kopioidaan.
Kohdetiedosto
    Tiedosto, johon kopioidaan.
    Tämä tiedosto ylikirjoitetaan, jos se on jo olemassa.

Huomautuksia:
    * Parametreissa --from-address ja --to-address ei-negatiiviset arvot
      tarkoittavat etäisyyttä tiedoston alusta: 0 = ensimmäinen tavu, 1 =
      toinen, jne. Negatiiviset arvot tarkoittavat etäisyyttä tiedoston
      lopusta: -1 = viimeinen tavu, -2 = toiseksi viimeinen, jne.
    * Oletuksena lähdetiedosto kopioidaan kokonaan kohdetiedostoon.
    * Numeeriset parametrit (--from-address, --to-address ja --length) voidaan
      antaa myös 16-kantaisina: esim. 0xff = 255 ja -0xff = -255.
    * Numeeristen parametrien lopussa saa olla jokin seuraavista liitteistä:
          K: kilo (2 ** 10)
          M: mega (2 ** 20)
          G: giga (2 ** 30)
          T: tera (2 ** 40)

Esimerkkitulostus

C:\>python fileslice.py --from-address=-0x10k -t-100 doomwad slice
Lähdetiedosto      : "doomwad"
Kohdetiedosto      : "slice"
Lähdetiedoston koko: 4196020 (0x4006b4)
Palan alkuosoite   : 4179636 (0x3fc6b4)
Palan loppuosoite  : 4195920 (0x400650)
Palan pituus       : 16285 (0x3f9d)
Kopioidaan...
Kopiointi valmis.

Kommentit

Metabolix [24.08.2015 11:30:04]

#

Muutama parannusehdotus:

qalle [25.08.2015 03:51:45]

#

Kiitos ehdotuksista, toteutin kolme ensimmäistä. Ohjelmaan saattoi jäädä bugeja, koska se on nyt monimutkaisempi.

qalle [27.08.2015 03:12:39]

#

Tein uuden version, jossa on suomenkieliset tekstit ja pari uutta ominaisuutta.

Chiman [29.08.2015 14:46:49]

#

Tyylillisen yhdenmukaisuuden takia laittaisin aina rivinvaihdon kaksoispisteen jälkeen eli esim. if-ehto ja ehdon takana oleva koodi eri riveille. Luettavuus paranee niin, vaikka koodin pituus hieman kasvaa.

Muuten koodi näyttää oikein selkeältä. Toimivuutta en testannut.

Kun minulla ei ole mitään muuta olennaista sanottavaa, viilaan pilkkua ja sanon että tämän rivin:

lengthInsteadOfEndPos = (endPosOrLength[0] == "+")

laittaisin näin:

lengthInsteadOfEndPos = endPosOrLength.startswith('+')

qalle [29.08.2015 17:01:15]

#

Kiitos, korjasin nuo ja pari muuta juttua.

qalle [14.09.2015 12:22:00]

#

Uusi versio 1.4: selkeytetty käyttäjän syöttämien lukujen tulkintaa (ConvertNumber()-funktio).

qalle [01.10.2017 02:00:13]

#

Uusi versio 1.5:

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta