diff --git a/README.md b/README.md index 03472ad..d238c02 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ alias show the real pathname linked to a virtual one backup backup the Directory IDs (required to decrypt names) in a ZIP file ``` -If no operation is specified, an interactive shell is launched on open vault. +If no operation is specified, an interactive shell is launched on open vault. It can do transparent wildcards expansion (`*` and `?` only). Functionality was tested in Windows 11 and Ubuntu 22.04 LTS Linux (under Windows WSL). diff --git a/pycryptomator/__init__.py b/pycryptomator/__init__.py index 3669f48..fe6688b 100644 --- a/pycryptomator/__init__.py +++ b/pycryptomator/__init__.py @@ -1,4 +1,4 @@ COPYRIGHT = '''Copyright (C)2024, by maxpat78.''' -__version__ = '1.7' +__version__ = '1.8' __all__ = ["Vault", "init_vault", "backupDirIds"] from .cryptomator import * diff --git a/pycryptomator/cryptomator.py b/pycryptomator/cryptomator.py index 7ecab56..1eca4f7 100644 --- a/pycryptomator/cryptomator.py +++ b/pycryptomator/cryptomator.py @@ -6,7 +6,7 @@ """ import getpass, hashlib, struct, base64 -import json, sys, io, os, operator +import json, sys, io, os, operator, re import time, zipfile, locale, uuid, shutil, fnmatch from os.path import * from itertools import groupby @@ -527,11 +527,12 @@ def ln(p, target, symlink): a = p.getInfo(symlink) if not exists(a.realPathName): os.mkdir(a.realPathName) out = open(join(a.realPathName, 'symlink.c9r'), 'wb') + if os.name == 'nt' and target[0] == '/': + target = calc_rel_path(target, symlink) + print("warning: absolute target pathname won't work with Windows") + print("relative conversion supplied:", target) Vault._encryptf(p.pk, io.BytesIO(target.encode()), out) # does not check target existance out.close() - b = p.getInfo(target) - if b.isDir: - shutil.copy(b.dirC9, a.realPathName) # copy the original dir.c9r also def ls(p, pathnames, opts): "List files and directories" @@ -925,3 +926,20 @@ def match(s, p=None): i+=1 #~ print ('fnmatch',aa,'against',bb,': matches') return 1 + +def calc_rel_path(base, child): + "returns the path of base relative to child" + base_parts = re.split(r'[\\/]+', abspath(base)) + child_parts = re.split(r'[\\/]+', abspath(child)) + # strips common subpath, if any + i=0 + while base_parts[i] == child_parts[i]: i += 1 + # returns base if they don't share anything + if not i: return base + n = len(child_parts) - 1 - i # counts path separators + relpath = '' + while n: + relpath += '../' + n -= 1 + relpath += '/'.join(base_parts[i:]) + return relpath diff --git a/pycryptomator/w32lex/__init__.py b/pycryptomator/w32lex/__init__.py index 36a24b1..99fd826 100644 --- a/pycryptomator/w32lex/__init__.py +++ b/pycryptomator/w32lex/__init__.py @@ -1,6 +1,6 @@ COPYRIGHT = '''Copyright (C)2024, by maxpat78.''' -__version__ = '1.0.3' +__version__ = '1.0.8' import os @@ -56,7 +56,10 @@ def split(s, mode=SPLIT_SHELL32): if not s: return argv # Special rules: - # Quotes: " open block; "" open and close block; """ open, add literal " and close block + # Quotes (consecutive or not): + # " open block; + # "" open and close block; + # """ open, add literal " and close block (not VC Runtime 2005+) # Backslashes, if followed by ": # 2n -> n, and open/close block # (2n+1) -> n, and add literal " @@ -151,33 +154,37 @@ def join(argv): "Quote and join list items, so that split returns the same" return ' '.join([quote(arg) for arg in argv]) + + +# +# cmd_ function are an attempt to provide a lexer/parser/tokenizer for Windows CMD +# + def cmd_parse(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): "Pre-process a command line like Windows CMD Command Prompt" escaped = 0 quoted = 0 percent = 0 exclamation = 0 - meta = 0 # special chars in a row + parenthesis = [] # opened parenthesis (argv position) arg = '' argv = [] - # is it right? handle ^CRLF? - s = s.strip('\r\n') + # ignore CR, should handle LF? + s = s.replace('\r','') # remove (ignore) some leading chars for c in ' ;,=\t\x0B\x0C\xFF': s = s.lstrip(c) if not s or s[0] == ':': return [] - # push special batch char - if s[0] == '@': + # push and strip special "line echo off" char + while s[0] == '@': argv = ['@'] s = s[1:] # some combinations at line start are prohibited if s[0] in '|&<>': raise NotExpected(s[0]) - if len(s)>1 and s[0:2] == '()': - raise NotExpected(')') i = 0 while i < len(s): @@ -193,11 +200,37 @@ def cmd_parse(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): else: escaped = 1 continue + if c == '(' and not (escaped or quoted): + if arg: + argv += [arg] + arg = '' + argv += [c] + parenthesis += [len(argv)-1] + continue + if c == ')' and not (escaped or quoted): + if arg: + argv += [arg] + arg = '' + if not parenthesis: + raise NotExpected(')') + last_opened = parenthesis.pop() + # replaces parenthesized trait with a single argument + argv[last_opened:] = [''.join(argv[last_opened:])+')'] + if argv[-1] == '()': + raise NotExpected('()') + continue + # at line start: abcd/e -> acd /e + if c == '/' and not (argv or quoted or ' ' in arg): + argv += [arg+' '] + arg = c + continue # %VAR% -> replace with os.environ['VAR'] *if set* and even if quoted # ^%VAR% -> same as above # %VAR^% # ^%VAR^% -> keep literal %VAR% # %%VAR%% -> replace internal %VAR% only + # NOTE: batch arguments %0..%9 and %* should be recognized? + # TBD: FOR parsing, %G and %%G and tilded vars if c == '%' and (mode&CMD_VAREXPAND): arg += c if percent and percent != i-1: @@ -222,27 +255,38 @@ def cmd_parse(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): continue exclamation = i # record exclamation mark position continue - # pipe, redirection, &, && and ||: break argument, and set aside special char/couple - # multiple pipe, redirection, &, && and || in sequence are forbidden - # TODO: recognize handle redirection "n>" and "n>&m" + # <,>,>>,&,&&,|,|| w/o blanks delimit 2 args + # " n>>&m" is the longest symbolic redirection + if c in '012' and s[i-2] == ' ' and i < len(s) and s[i] in '<>': + n=i+1 # index of next char in sequence + if s[i] == '>' and n < len(s) and s[n] == '>': # optional 2nd > + n+=1 + # note: cmd recognizes n>^&m as valid as n>&m (!) + if n+3 < len(s) and s[n] == '^' and s[n+1] == '&' and s[n+2] in '012': + n+=3 + if n+2 < len(s) and s[n] == '&' and s[n+1] in '012': + n+=2 + if arg: argv += [arg] + arg = '' + argv += [s[i-1:n].replace('^','')] # eventually fix weird case above + i = n + continue if c in '|<>&': if escaped or quoted: arg += c escaped = 0 continue - meta += 1 - # 3 specials in a row is forbidden - if meta == 3: raise NotExpected(c) - # if 2 specials undoubled - if len(argv) >= 2 and argv[-1] in '|<>&' and c != argv[-1]: raise NotExpected(c) - # push argument, if any, and special char/couple if arg: argv += [arg] - argv += [c] - # if doubled operator: ||, <<, >>, && - if i < len(s) and s[i] == c: - argv[-1] = 2*c - i += 1 - meta += 1 + arg = '' + if i < len(s) and s[i] != '<' and s[i] == c: # if doubled + arg = 2*c + i+=1 + else: + arg += c + if arg in ('>','<','>>','<<') and i < len(s) and s[i] == '&' and s[i+1] in '012': # if valid handle redir + arg += '&'+s[i+1] + i+=2 + argv += [arg] arg = '' continue if c in ' ,;=\t': @@ -252,15 +296,16 @@ def cmd_parse(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): argv += [c] escaped = 0 continue - else: - meta = 0 arg += c escaped = 0 - argv += [arg] + if arg: argv += [arg] + # if any unclosed parenthesis + if parenthesis: + raise NotExpected('(') return argv -def cmd_split(s, mode=SPLIT_SHELL32): - "Post-process with split a command line parsed by cmd_parse (mimic mslex behavior)" +def cmd_split(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): + "Post-process with split a command line parsed by cmd_parse" argv = [] for tok in cmd_parse(s, mode): if tok in ('@','<','|','>','<<','>>','&','&&','||'):