1+ import logging
12import re
23import subprocess
34from os import PathLike
67
78import click
89import yaml
10+ from cookiecutter .main import cookiecutter
911from slugify import slugify
1012
1113from ctfcli .core .api import API
1214from ctfcli .core .exceptions import (
15+ ChallengeException ,
1316 InvalidChallengeDefinition ,
1417 InvalidChallengeFile ,
1518 LintException ,
1922from ctfcli .utils .hashing import hash_file
2023from ctfcli .utils .tools import strings
2124
25+ log = logging .getLogger ("ctfcli.core.challenge" )
26+
2227
2328def str_presenter (dumper , data ):
2429 if len (data .splitlines ()) > 1 or "\n " in data :
@@ -100,6 +105,43 @@ def is_default_challenge_property(key: str, value: Any) -> bool:
100105
101106 return False
102107
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+
103145 @property
104146 def api (self ):
105147 if not self ._api :
@@ -110,6 +152,7 @@ def api(self):
110152 # __init__ expects an absolute path to challenge_yml, or a relative one from the cwd
111153 # it does not join that path with the project_path
112154 def __init__ (self , challenge_yml : Union [str , PathLike ], overrides = None ):
155+ log .debug (f"Challenge.__init__: ({ challenge_yml = } , { overrides = } " )
113156 if overrides is None :
114157 overrides = {}
115158
@@ -209,7 +252,7 @@ def _load_challenge_id(self):
209252
210253 def _validate_files (self ):
211254 # 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" , []) :
213256 if not (self .challenge_directory / challenge_file ).exists ():
214257 raise InvalidChallengeFile (f"File { challenge_file } could not be loaded" )
215258
0 commit comments