- Code cleanup.
- Removed the need for local temp files.
This commit is contained in:
parent
470da10745
commit
eec5057460
@ -4,24 +4,6 @@
|
|||||||
#
|
#
|
||||||
# (c)2004 Cas Cremers
|
# (c)2004 Cas Cremers
|
||||||
#
|
#
|
||||||
# Input of this script:
|
|
||||||
#
|
|
||||||
# - A number on the commandline of stuff to test
|
|
||||||
# - A list of files on stdin to be used (lines starting with '#' are
|
|
||||||
# ignored)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# Tips and tricks:
|
|
||||||
#
|
|
||||||
# Use e.g.
|
|
||||||
# $ ulimit -v 100000
|
|
||||||
# to counteract memory problems
|
|
||||||
#
|
|
||||||
# If you know where to look, use
|
|
||||||
# $ ls s*.spdl t*.spdl -1 | ./multiprotocoltest.py 2
|
|
||||||
# To verify combos of protocols starting with s and t
|
|
||||||
#
|
|
||||||
|
|
||||||
# ***********************
|
# ***********************
|
||||||
# MODULES
|
# MODULES
|
||||||
# ***********************
|
# ***********************
|
||||||
@ -31,9 +13,9 @@ import sys
|
|||||||
import string
|
import string
|
||||||
import commands
|
import commands
|
||||||
import copy
|
import copy
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
# My own stuff
|
||||||
import tuplesdo
|
import tuplesdo
|
||||||
import scythertest
|
import scythertest
|
||||||
import protocollist
|
import protocollist
|
||||||
@ -43,28 +25,12 @@ import protocollist
|
|||||||
# PARAMETERS
|
# PARAMETERS
|
||||||
# ***********************
|
# ***********************
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
TempFileList = "scyther-blap.tmp"
|
|
||||||
TempFileTuples = "scyther-blip.tmp"
|
|
||||||
|
|
||||||
# External programs
|
|
||||||
TupleProgram = "./tuples.py"
|
|
||||||
|
|
||||||
# Some default settings for Agents, untrusted e with sk(e) and k(a,e) etc.
|
|
||||||
ScytherDefaults = "--summary"
|
|
||||||
IncludeProtocols = '../spdl/spdl-defaults.inc'
|
|
||||||
|
|
||||||
# Some protocols are causing troubles: this is a hard-coded filter to exclude
|
|
||||||
# the problem children. Unfair, yes. Practical, yes.
|
|
||||||
#SkipList = [ 'gong-nonce.spdl', 'gong-nonce-b.spdl', 'splice-as-hc.spdl', 'kaochow-palm.spdl' ]
|
|
||||||
SkipList = []
|
|
||||||
|
|
||||||
ClaimToResultMap = {} # maps protocol claims to correctness in singular tests (0,1)
|
ClaimToResultMap = {} # maps protocol claims to correctness in singular tests (0,1)
|
||||||
ProtocolToFileMap = {} # maps protocol names to file names
|
ProtocolToFileMap = {} # maps protocol names to file names
|
||||||
ProtocolToStatusMap = {} # maps protocol names to status: 0 all false, 1 all correct, otherwise (2) mixed
|
ProtocolToStatusMap = {} # maps protocol names to status: 0 all false, 1 all correct, otherwise (2) mixed
|
||||||
ProtocolToEffectsMap = {} # maps protocols that help create multiple flaws, to the protocol names of the flaws they caused
|
ProtocolToEffectsMap = {} # maps protocols that help create multiple flaws, to the protocol names of the flaws they caused
|
||||||
|
|
||||||
CommandPrefix = "not yet initialised."
|
CommandPrefix = ""
|
||||||
ArgumentsList = [] # argument lists that have been displayed onscreen
|
ArgumentsList = [] # argument lists that have been displayed onscreen
|
||||||
|
|
||||||
# Ugly hack. Works.
|
# Ugly hack. Works.
|
||||||
@ -122,6 +88,8 @@ def PrintProtStatus (file, prname):
|
|||||||
# Returns a dictionary of claim -> bool; where 1 means that it is
|
# Returns a dictionary of claim -> bool; where 1 means that it is
|
||||||
# correct, and 0 means that it is false (i.e. there exists an attack)
|
# correct, and 0 means that it is false (i.e. there exists an attack)
|
||||||
def ScytherEval (plist):
|
def ScytherEval (plist):
|
||||||
|
global options
|
||||||
|
|
||||||
# Flush before trying (possibly fatal) external commands
|
# Flush before trying (possibly fatal) external commands
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
@ -173,6 +141,8 @@ LastProgress = {}
|
|||||||
ProgressBarWidth = 50
|
ProgressBarWidth = 50
|
||||||
|
|
||||||
def ShowProgress (i,n,txt):
|
def ShowProgress (i,n,txt):
|
||||||
|
global options
|
||||||
|
|
||||||
def IntegerPart (x):
|
def IntegerPart (x):
|
||||||
return int (( x * i ) / n)
|
return int (( x * i ) / n)
|
||||||
|
|
||||||
@ -181,13 +151,13 @@ def ShowProgress (i,n,txt):
|
|||||||
percentage = IntegerPart (100)
|
percentage = IntegerPart (100)
|
||||||
factor = IntegerPart (ProgressBarWidth)
|
factor = IntegerPart (ProgressBarWidth)
|
||||||
|
|
||||||
showme = 0
|
showme = False
|
||||||
if LastProgress.has_key(n):
|
if LastProgress.has_key(n):
|
||||||
if LastProgress[n]<>(factor,txt):
|
if LastProgress[n]<>(factor,txt):
|
||||||
showme = 1
|
showme = True
|
||||||
else:
|
else:
|
||||||
showme = 1
|
showme = True
|
||||||
if showme == 1:
|
if showme:
|
||||||
bar = "\r["
|
bar = "\r["
|
||||||
i = 0
|
i = 0
|
||||||
while i < ProgressBarWidth:
|
while i < ProgressBarWidth:
|
||||||
@ -202,6 +172,8 @@ def ShowProgress (i,n,txt):
|
|||||||
LastProgress[n] = (factor, txt)
|
LastProgress[n] = (factor, txt)
|
||||||
|
|
||||||
def ClearProgress (n,txt):
|
def ClearProgress (n,txt):
|
||||||
|
global options
|
||||||
|
|
||||||
if not options.progressbar:
|
if not options.progressbar:
|
||||||
return
|
return
|
||||||
bar = " " * (1 + ProgressBarWidth + 2 + 5 + len(txt))
|
bar = " " * (1 + ProgressBarWidth + 2 + 5 + len(txt))
|
||||||
@ -279,7 +251,7 @@ def DescribeContext (filep, protocols, claim):
|
|||||||
# the protocol of the claim. However, if the
|
# the protocol of the claim. However, if the
|
||||||
# partner protocol is completely correct or
|
# partner protocol is completely correct or
|
||||||
# completely false, we summarize.
|
# completely false, we summarize.
|
||||||
summary = 0
|
summary = False
|
||||||
all = 0
|
all = 0
|
||||||
if claim.split()[0] <> prname:
|
if claim.split()[0] <> prname:
|
||||||
count = [0,0]
|
count = [0,0]
|
||||||
@ -287,12 +259,12 @@ def DescribeContext (filep, protocols, claim):
|
|||||||
count[v] = count[v]+1
|
count[v] = count[v]+1
|
||||||
if count[0] == 0 and count[1] > 0:
|
if count[0] == 0 and count[1] > 0:
|
||||||
all = 1
|
all = 1
|
||||||
summary = 1
|
summary = True
|
||||||
if count[1] == 0 and count[0] > 0:
|
if count[1] == 0 and count[0] > 0:
|
||||||
all = 0
|
all = 0
|
||||||
summary = 1
|
summary = True
|
||||||
|
|
||||||
if summary == 1:
|
if summary:
|
||||||
DC_Claim (cl.split()[0] + " *ALL*", all)
|
DC_Claim (cl.split()[0] + " *ALL*", all)
|
||||||
else:
|
else:
|
||||||
for cl,v in cllist:
|
for cl,v in cllist:
|
||||||
@ -315,7 +287,7 @@ def RequiresAllProtocols (protocols, claim):
|
|||||||
# claim was always false
|
# claim was always false
|
||||||
return 0
|
return 0
|
||||||
# check for simple cases
|
# check for simple cases
|
||||||
if int(TupleWidth) <= 2:
|
if TupleWidth <= 2:
|
||||||
# nothing to remove
|
# nothing to remove
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@ -387,118 +359,104 @@ def SignalAttack (protocols, claim):
|
|||||||
#
|
#
|
||||||
# Furthermore, TempFileList is created.
|
# Furthermore, TempFileList is created.
|
||||||
|
|
||||||
parser = OptionParser()
|
def main():
|
||||||
scythertest.default_options(parser)
|
global options
|
||||||
parser.add_option("-t","--tuplewidth", dest="tuplewidth",
|
global processed, newattacks, StartSkip
|
||||||
|
global TupleWidth, TupleCount
|
||||||
|
|
||||||
|
parser = OptionParser()
|
||||||
|
scythertest.default_options(parser)
|
||||||
|
parser.add_option("-t","--tuplewidth", dest="tuplewidth",
|
||||||
default = 2,
|
default = 2,
|
||||||
help = "number of concurrent protocols to test, >=2")
|
help = "number of concurrent protocols to test, >=2")
|
||||||
parser.add_option("-p","--protocols", dest="protocols",
|
parser.add_option("-p","--protocols", dest="protocols",
|
||||||
default = 0,
|
default = 0,
|
||||||
help = "protocol selection (0: all, 1:literature only)")
|
help = "protocol selection (0: all, 1:literature only)")
|
||||||
parser.add_option("-s","--start", dest="startpercentage",
|
parser.add_option("-s","--start", dest="startpercentage",
|
||||||
default = 0,
|
default = 0,
|
||||||
help = "start test at a certain percentage")
|
help = "start test at a certain percentage")
|
||||||
parser.add_option("-B","--disable-progressbar", dest="progressbar",
|
parser.add_option("-B","--disable-progressbar", dest="progressbar",
|
||||||
default = "True",
|
default = "True",
|
||||||
action = "store_false",
|
action = "store_false",
|
||||||
help = "suppress a progress bar")
|
help = "suppress a progress bar")
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
# Where should we start (if this is a number)
|
# Where should we start (if this is a number)
|
||||||
StartPercentage = int (options.startpercentage)
|
StartPercentage = int (options.startpercentage)
|
||||||
if StartPercentage < 0 or StartPercentage > 100:
|
if StartPercentage < 0 or StartPercentage > 100:
|
||||||
print "Illegal range for starting percentage (0-100):", StartPercentage
|
print "Illegal range for starting percentage (0-100):", StartPercentage
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
# Send protocollist to temp file (is this necessary?)
|
# Send protocollist to temp file (is this necessary?)
|
||||||
ProtocolFileList = protocollist.select(options.protocols)
|
ProtocolFileList = protocollist.select(options.protocols)
|
||||||
ProtocolCount = len(ProtocolFileList)
|
ProtocolCount = len(ProtocolFileList)
|
||||||
outp = open(TempFileList, 'w')
|
|
||||||
for l in ProtocolFileList:
|
|
||||||
outp.write(l + "\n")
|
|
||||||
outp.close()
|
|
||||||
|
|
||||||
# Determine arguments
|
# Determine arguments
|
||||||
TupleWidth = str(options.tuplewidth)
|
TupleWidth = int(options.tuplewidth)
|
||||||
|
|
||||||
# Match
|
# Match
|
||||||
ScytherMethods = "--match=" + str(options.match)
|
ScytherMethods = "--match=" + str(options.match)
|
||||||
|
|
||||||
# Method of bounding will be determined in ScytherEval
|
# Method of bounding will be determined in ScytherEval
|
||||||
|
|
||||||
# Caching of single-protocol results for speed gain.
|
# Caching of single-protocol results for speed gain.
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# The script first computes the singular results for all the protocols
|
# The script first computes the singular results for all the protocols
|
||||||
# and stores this in an array, or something like that.
|
# and stores this in an array, or something like that.
|
||||||
|
|
||||||
print "Evaluating tuples of", TupleWidth, "for", ProtocolCount, "protocols, using the command '" + CommandPrefix + "'"
|
TupleCount = tuplesdo.tuples_count(ProtocolCount, TupleWidth)
|
||||||
i = 0
|
print "Evaluating", TupleCount, "tuples of", TupleWidth, "for", ProtocolCount, "protocols."
|
||||||
while i < ProtocolCount:
|
i = 0
|
||||||
|
while i < ProtocolCount:
|
||||||
ShowProgress (i, ProtocolCount,ProtocolFileList[i]+safetxt)
|
ShowProgress (i, ProtocolCount,ProtocolFileList[i]+safetxt)
|
||||||
ScytherEval1 ( ProtocolFileList[i] )
|
ScytherEval1 ( ProtocolFileList[i] )
|
||||||
i = i + 1
|
i = i + 1
|
||||||
ClearProgress(ProtocolCount, safetxt)
|
ClearProgress(ProtocolCount, safetxt)
|
||||||
print "Evaluated single results."
|
print "Evaluated single results."
|
||||||
|
|
||||||
# Show classification
|
# Show classification
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
print "Correct protocols: ", GetKeys (ProtocolToStatusMap, 1)
|
print "Correct protocols: ", GetKeys (ProtocolToStatusMap, 1)
|
||||||
print "Partly flawed protocols: ", GetKeys (ProtocolToStatusMap, 2)
|
print "Partly flawed protocols: ", GetKeys (ProtocolToStatusMap, 2)
|
||||||
print "Completely flawed protocols: ", GetKeys (ProtocolToStatusMap, 0)
|
print "Completely flawed protocols: ", GetKeys (ProtocolToStatusMap, 0)
|
||||||
|
|
||||||
# Computation of combined list.
|
# Testing of protocol tuples
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# We use the tuple script to generate the list of tuples we need.
|
# We take the list of tuples and test each combination.
|
||||||
# We use a temporary file (again) to store that list.
|
|
||||||
# This requires that 'tuples.py' is in the same directory.
|
|
||||||
|
|
||||||
lstatus=os.system(TupleProgram + ' ' + TupleWidth + ' <' + TempFileList + ' >' + TempFileTuples)
|
processed = 0
|
||||||
|
newattacks = 0
|
||||||
|
StartSkip = 0
|
||||||
|
|
||||||
inp = open(TempFileTuples, 'r')
|
# Possibly skip some
|
||||||
TupleCount = 0
|
if StartPercentage > 0:
|
||||||
for x in inp:
|
|
||||||
TupleCount = TupleCount + 1
|
|
||||||
inp.close()
|
|
||||||
print "Commencing test for", TupleCount, "protocol combinations."
|
|
||||||
|
|
||||||
# Testing of protocol tuples
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
#
|
|
||||||
# We take the list of tuples and test each combination.
|
|
||||||
|
|
||||||
inp = open(TempFileTuples, 'r')
|
|
||||||
processed = 0
|
|
||||||
newattacks = 0
|
|
||||||
StartSkip = 0
|
|
||||||
|
|
||||||
# Possibly skip some
|
|
||||||
if StartPercentage > 0:
|
|
||||||
StartSkip = int ((TupleCount * StartPercentage) / 100)
|
StartSkip = int ((TupleCount * StartPercentage) / 100)
|
||||||
print "Resuming. Skipping the first", StartSkip,"tuples."
|
print "Resuming. Skipping the first", StartSkip,"tuples."
|
||||||
|
|
||||||
#
|
#
|
||||||
# Check all these protocols
|
# Check all these protocols
|
||||||
#
|
#
|
||||||
for tline in inp:
|
def process(protocols):
|
||||||
|
global processed, newattacks, StartSkip
|
||||||
|
|
||||||
if (processed >= StartSkip):
|
if (processed >= StartSkip):
|
||||||
#
|
#
|
||||||
# Get the next tuple
|
# Get the next tuple
|
||||||
#
|
#
|
||||||
protocols = tline.split()
|
|
||||||
ShowProgress (processed, TupleCount, " ".join(protocols) + safetxt)
|
ShowProgress (processed, TupleCount, " ".join(protocols) + safetxt)
|
||||||
#
|
#
|
||||||
# Determine whether there are valid claims at all in
|
# Determine whether there are valid claims at all in
|
||||||
# this set of file names
|
# this set of file names
|
||||||
#
|
#
|
||||||
has_valid_claims = 0
|
has_valid_claims = False
|
||||||
for prname in GetListKeys (ProtocolToFileMap, protocols):
|
for prname in GetListKeys (ProtocolToFileMap, protocols):
|
||||||
if ProtocolToStatusMap[prname] != 0:
|
if ProtocolToStatusMap[prname] != 0:
|
||||||
has_valid_claims = 1
|
has_valid_claims = True
|
||||||
if has_valid_claims == 1:
|
if has_valid_claims:
|
||||||
#
|
#
|
||||||
# Use Scyther to verify the claims
|
# Use Scyther to verify the claims
|
||||||
#
|
#
|
||||||
@ -515,19 +473,24 @@ for tline in inp:
|
|||||||
|
|
||||||
# Next!
|
# Next!
|
||||||
processed = processed + 1
|
processed = processed + 1
|
||||||
inp.close()
|
|
||||||
|
|
||||||
ClearProgress (TupleCount, safetxt)
|
tuplesdo.tuples_do(process,ProtocolFileList,TupleWidth)
|
||||||
print "Processed", processed,"tuple combinations in total."
|
|
||||||
if StartSkip > 0:
|
ClearProgress (TupleCount, safetxt)
|
||||||
|
print "Processed", processed,"tuple combinations in total."
|
||||||
|
if StartSkip > 0:
|
||||||
print "In this session, checked the last",(processed - StartSkip),"tuples. "
|
print "In this session, checked the last",(processed - StartSkip),"tuples. "
|
||||||
print "Found", newattacks, "new attacks."
|
print "Found", newattacks, "new attacks."
|
||||||
if newattacks > 0:
|
if newattacks > 0:
|
||||||
print " These were helped by:"
|
print " These were helped by:"
|
||||||
for helper in ProtocolToEffectsMap.keys():
|
for helper in ProtocolToEffectsMap.keys():
|
||||||
sys.stdout.write (" ")
|
sys.stdout.write (" ")
|
||||||
PrintProtStatus (sys.stdout, helper)
|
PrintProtStatus (sys.stdout, helper)
|
||||||
sys.stdout.write (". This possibly breaks " + str(ProtocolToEffectsMap[helper]) + "\n")
|
sys.stdout.write (". This possibly breaks " + str(ProtocolToEffectsMap[helper]) + "\n")
|
||||||
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
# Tuple module
|
|
||||||
#
|
|
||||||
# tuplesDo generates all unordered sets (in a list) of size n of the
|
|
||||||
# elements of the list l. The resulting lists (of length n) are passed
|
|
||||||
# to the function f.
|
|
||||||
|
|
||||||
def tuplesDo (f,l,n):
|
|
||||||
def tuplesDoRecurse (l,r):
|
|
||||||
if r and (len(r) == n):
|
|
||||||
f(r)
|
|
||||||
else:
|
|
||||||
if l and (n > 0):
|
|
||||||
# Larger size: we have options
|
|
||||||
# Option 1: include first
|
|
||||||
tuplesDoRecurse (l[1:], r + [l[0]])
|
|
||||||
# Option 2: exclude first
|
|
||||||
tuplesDoRecurse (l[1:], r)
|
|
||||||
|
|
||||||
tuplesDoRecurse (l,[])
|
|
||||||
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
#
|
|
||||||
# Given a number of input lines on std and an argument int, this program
|
|
||||||
# generates unordered tuples, e.g.:
|
|
||||||
#
|
|
||||||
# arg: 2
|
|
||||||
# in: a
|
|
||||||
# b
|
|
||||||
# c
|
|
||||||
# d
|
|
||||||
#
|
|
||||||
# out: a,b
|
|
||||||
# a,c
|
|
||||||
# a,d
|
|
||||||
# b,c
|
|
||||||
# b,d
|
|
||||||
# c,d
|
|
||||||
#
|
|
||||||
# This should make it clear what happens.
|
|
||||||
#
|
|
||||||
import sys
|
|
||||||
import string
|
|
||||||
import tuplesdo
|
|
||||||
|
|
||||||
# Retrieve the tuple width
|
|
||||||
tuplesize = int(sys.argv[1])
|
|
||||||
|
|
||||||
# Read stdin into list and count
|
|
||||||
list = []
|
|
||||||
loop = 1
|
|
||||||
while loop:
|
|
||||||
line = sys.stdin.readline()
|
|
||||||
if line != '':
|
|
||||||
# not the end of the input
|
|
||||||
line = string.strip(line)
|
|
||||||
if line != '':
|
|
||||||
# not a blank line
|
|
||||||
list.append(line)
|
|
||||||
else:
|
|
||||||
# end of the input
|
|
||||||
loop = 0
|
|
||||||
|
|
||||||
def tuplesPrint (l, n):
|
|
||||||
def f (resultlist):
|
|
||||||
print " ".join(resultlist)
|
|
||||||
|
|
||||||
tuplesdo.tuplesDo (f, l, n)
|
|
||||||
|
|
||||||
# Generate tuples...
|
|
||||||
tuplesPrint (list, tuplesize)
|
|
||||||
# Thanks for your attention
|
|
@ -4,18 +4,41 @@
|
|||||||
# elements of the list l. The resulting lists (of length n) are passed
|
# elements of the list l. The resulting lists (of length n) are passed
|
||||||
# to the function f.
|
# to the function f.
|
||||||
|
|
||||||
def tuplesDo (f,l,n):
|
|
||||||
def tuplesDoRecurse (l,r):
|
# First some generic combinatorial stuff
|
||||||
|
|
||||||
|
def faculty_gen(n,k):
|
||||||
|
if n <= k:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return n * faculty_gen(n-1,k)
|
||||||
|
|
||||||
|
def faculty(n):
|
||||||
|
return faculty_gen(n,1)
|
||||||
|
|
||||||
|
def binomial(n,k):
|
||||||
|
b1 = faculty_gen(n,k)
|
||||||
|
b2 = faculty(n-k)
|
||||||
|
return b1/b2
|
||||||
|
|
||||||
|
|
||||||
|
# How many elements will there be?
|
||||||
|
def tuples_count (l,n):
|
||||||
|
return binomial(l,n)
|
||||||
|
|
||||||
|
# Generate those elements, and apply f
|
||||||
|
def tuples_do (f,l,n):
|
||||||
|
def recurse (l,r):
|
||||||
if r and (len(r) == n):
|
if r and (len(r) == n):
|
||||||
f(r)
|
f(r)
|
||||||
else:
|
else:
|
||||||
if l and (n > 0):
|
if l and (n > 0):
|
||||||
# Larger size: we have options
|
# Larger size: we have options
|
||||||
# Option 1: include first
|
# Option 1: include first
|
||||||
tuplesDoRecurse (l[1:], r + [l[0]])
|
recurse (l[1:], r + [l[0]])
|
||||||
# Option 2: exclude first
|
# Option 2: exclude first
|
||||||
tuplesDoRecurse (l[1:], r)
|
recurse (l[1:], r)
|
||||||
|
|
||||||
tuplesDoRecurse (l,[])
|
recurse (l,[])
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user