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:
"""
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()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)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.
Muutama parannusehdotus:
Kiitos ehdotuksista, toteutin kolme ensimmäistä. Ohjelmaan saattoi jäädä bugeja, koska se on nyt monimutkaisempi.
Tein uuden version, jossa on suomenkieliset tekstit ja pari uutta ominaisuutta.
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('+')Kiitos, korjasin nuo ja pari muuta juttua.
Uusi versio 1.4: selkeytetty käyttäjän syöttämien lukujen tulkintaa (ConvertNumber()-funktio).
Uusi versio 1.5:
with-käskyä tiedostojen käsittelyyn