1
+ import logging
1
2
import re
2
3
import subprocess
3
4
from os import PathLike
6
7
7
8
import click
8
9
import yaml
10
+ from cookiecutter .main import cookiecutter
9
11
from slugify import slugify
10
12
11
13
from ctfcli .core .api import API
12
14
from ctfcli .core .exceptions import (
15
+ ChallengeException ,
13
16
InvalidChallengeDefinition ,
14
17
InvalidChallengeFile ,
15
18
LintException ,
19
22
from ctfcli .utils .hashing import hash_file
20
23
from ctfcli .utils .tools import strings
21
24
25
+ log = logging .getLogger ("ctfcli.core.challenge" )
26
+
22
27
23
28
def str_presenter (dumper , data ):
24
29
if len (data .splitlines ()) > 1 or "\n " in data :
@@ -100,6 +105,43 @@ def is_default_challenge_property(key: str, value: Any) -> bool:
100
105
101
106
return False
102
107
108
+ @staticmethod
109
+ def clone (config , remote_challenge ):
110
+ name = remote_challenge ["name" ]
111
+
112
+ if name is None :
113
+ raise ChallengeException (f'Could not get name of remote challenge with id { remote_challenge ["id" ]} ' )
114
+
115
+ # First, generate a name for the challenge directory
116
+ category = remote_challenge .get ("category" , None )
117
+ challenge_dir_name = slugify (name )
118
+ if category is not None :
119
+ challenge_dir_name = str (Path (slugify (category )) / challenge_dir_name )
120
+
121
+ if Path (challenge_dir_name ).exists ():
122
+ raise ChallengeException (
123
+ f"Challenge directory '{ challenge_dir_name } ' for challenge '{ name } ' already exists"
124
+ )
125
+
126
+ # Create an blank/empty challenge, with only the challenge.yml containing the challenge name
127
+ template_path = config .get_base_path () / "templates" / "blank" / "empty"
128
+ log .debug (f"Challenge.clone: cookiecutter({ str (template_path )} , { name = } , { challenge_dir_name = } " )
129
+ cookiecutter (
130
+ str (template_path ),
131
+ no_input = True ,
132
+ extra_context = {"name" : name , "dirname" : challenge_dir_name },
133
+ )
134
+
135
+ if not Path (challenge_dir_name ).exists ():
136
+ raise ChallengeException (f"Could not create challenge directory '{ challenge_dir_name } ' for '{ name } '" )
137
+
138
+ # Add the newly created local challenge to the config file
139
+ config ["challenges" ][challenge_dir_name ] = challenge_dir_name
140
+ with open (config .config_path , "w+" ) as f :
141
+ config .write (f )
142
+
143
+ return Challenge (f"{ challenge_dir_name } /challenge.yml" )
144
+
103
145
@property
104
146
def api (self ):
105
147
if not self ._api :
@@ -110,6 +152,7 @@ def api(self):
110
152
# __init__ expects an absolute path to challenge_yml, or a relative one from the cwd
111
153
# it does not join that path with the project_path
112
154
def __init__ (self , challenge_yml : Union [str , PathLike ], overrides = None ):
155
+ log .debug (f"Challenge.__init__: ({ challenge_yml = } , { overrides = } " )
113
156
if overrides is None :
114
157
overrides = {}
115
158
@@ -209,7 +252,7 @@ def _load_challenge_id(self):
209
252
210
253
def _validate_files (self ):
211
254
# if the challenge defines files, make sure they exist before making any changes to the challenge
212
- for challenge_file in self [ "files" ] :
255
+ for challenge_file in self . get ( "files" , []) :
213
256
if not (self .challenge_directory / challenge_file ).exists ():
214
257
raise InvalidChallengeFile (f"File { challenge_file } could not be loaded" )
215
258
0 commit comments