Xit/xit.py

146 lines
5.5 KiB
Python
Raw Normal View History

#!/usr/bin/python3
""" xit
Usage:
xit (<FILE>)... [--size <SIZE>] [--seed <SEED>] [--overwrite] [--help]
XOR(in files) == XOR(out files)
Use cases:
$ xit A ^ A1 A2 # splits the secret A into two chuncks A1 and A2
$ xit A1 A2 ^ A # gets the secret back, turns A1 and A2 into A
$ xit ^ R1 R2 R3 # generates random files R1, R2, and R3
The files at the LEFT of ^ are the INput files.
The files at the RIGHT of ^ are the OUTput files.
Special cases:
$ xit A1 A2 ^ B1 B2 # A1 xor A2 == B1 xor B2
$ xit C ^ B A-{3} # generates 4 files: A1, A-|1|, A-|2|, A-|3|
# So, C == B ^ A-|1| ^ A-|2| ^ A-|3|
$ xit C ^ B A-{2} B-{2} # generates 5 files:
# So, C == B ^ A-|1| ^ A-|2|
# AND: C == B ^ B-|1| ^ B-|2|
|x| means the sha1 digest, included in the file name, of the file itself.
Arguments:
size 'even': assumes all input files are of the same size [default]
'min': keeps the smallest size, truncate others if needed
'max': keeps the biggest size, pad others if needed
integer: sets all files at the same size, truncating or padding
FILE A bunch of files, first the input files then the output files.
Separated by ^. Without input files, it generates random files.
input files Enclosed in underscores (_) means a string as a key
Otherwise, means a readable file
output files With one {nb} means a multiple output, more than one is an error
Otherwise, means a writable file
Options:
--size, -s SIZE input size: first, min, max, even [default: even], last or a size
--seed, -e SEED Uses a seed to generate test randomness, a zero triggers the
Python's internal Random instead of urandom
--overwrite, -w Allows overwritting of existing output files
"""
import sys, re, random
from docopt import docopt
from pathlib import Path
from collections import Counter
import lib_xit
import commands
import utils
if __name__ != '__main__': sys.exit()
def manageFileErrors(inArgs, outFiles, outSha1, overwrite, size):
errors = {
'Input files not found':
[a for a in inArgs if not Path(a).is_file()],
'Output files already exist (see --overwrite)':
[] if overwrite else [a for a in outFiles if Path(a).is_file()],
'Duplicate input files':
[f for f,count in
Counter([str(Path(a).resolve()) for a in inArgs]).items()
if count>1],
'Duplicate output files':
[f for f,count in Counter(outFiles).items() if count>1],
'Files both input AND output':
list(set(inArgs) & set(outFiles)),
'Input files have different sizes (see --size)':
[] if size!='even'
else len(set((Path(a).lstat().st_size for a in inArgs if Path(a).is_file()))) > 1 and inArgs,
'Some input files have null size':
[f for f in inArgs if Path(f).is_file() and Path(f).lstat().st_size==0],
}
return errors
arguments = docopt(__doc__)
files, overwrite, size, seed = (
arguments[a]
for a in ('<FILE>', '--overwrite', '--size', '--seed')
)
if seed: # This makes predictable outputs for random generators! ONLY usefull for test purpose!
if int(seed): random.seed(seed) # A zero just forces the use of Python's random instead of urandom
lib_xit.useUrandom = False
try:
try:
inArgs, outArgs = utils.splitList(files, '^')
except ValueError:
raise ValueError('Exactly one ^ required!')
if not outArgs:
raise ValueError('There must be at least an output after ^')
inFileArgs, inStrArgs = [], []
for i in inArgs:
m = utils.StrKey(i)
if m:
inStrArgs.append(str(m))
else:
inFileArgs.append(i)
outFiles, outSha1Files = [], []
for o in outArgs:
s = utils.Sha1File(o)
if s:
outSha1Files.append(s)
else:
outFiles.append(o)
# Prevents output file being the same as the input file
if len(inArgs)==1 : # Only one input, file. If there are two, it can be a simple xor.
if not len(outFiles): # Without normal file, all {1} sha1 file will be the same as the input file
for o in outSha1Files:
if int(o) == 1:
raise OSError('The output file will have the same content as the input file!')
elif len(outFiles)==1 and not outSha1Files: # One input file and one output file
raise OSError('The output file will have the same content as the input file!')
sizesAvailable = ('first', 'even', 'min', 'max', 'last')
size = size.lower()
if size not in sizesAvailable:
try:
size = utils.evalMultiple(size)
except ValueError:
raise ValueError('size is not an int and not in: '+', '.join(sizesAvailable))
if size <= 0:
raise ValueError('size must be strictly positive!')
assert type(size) is int or size in sizesAvailable
if outSha1Files: overwrite = True # As sha1 is known after the file checks...
errors = manageFileErrors(inFileArgs, outFiles, outSha1Files, overwrite, size)
if any(errors.values()):
for error, files in errors.items():
if files:
sys.stderr.write(error+':\n')
for e in files: sys.stderr.write(' '+e+'\n')
commands.xorFiles(inStrArgs, inFileArgs, outFiles, outSha1Files, size)
except (OSError, ValueError, IndexError) as e:
sys.exit(e)