Skip to content

Commit

Permalink
fixed ln
Browse files Browse the repository at this point in the history
- a directory symlink does not copy dir.c9r anymore
- forbid an absolute symbolic link target (under Windows) and replaces it with a relative version
- w32lex updated to 1.0.8
  • Loading branch information
maxpat78 committed Oct 26, 2024
1 parent 03e7050 commit c626d2f
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 35 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion pycryptomator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
COPYRIGHT = '''Copyright (C)2024, by maxpat78.'''
__version__ = '1.7'
__version__ = '1.8'
__all__ = ["Vault", "init_vault", "backupDirIds"]
from .cryptomator import *
26 changes: 22 additions & 4 deletions pycryptomator/cryptomator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
103 changes: 74 additions & 29 deletions pycryptomator/w32lex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
COPYRIGHT = '''Copyright (C)2024, by maxpat78.'''

__version__ = '1.0.3'
__version__ = '1.0.8'

import os

Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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':
Expand All @@ -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 ('@','<','|','>','<<','>>','&','&&','||'):
Expand Down

0 comments on commit c626d2f

Please sign in to comment.