1- use crate :: paths:: InstallPaths ;
1+ use crate :: paths:: { validate_critical_path , InstallPaths } ;
22use crate :: state:: { ApacheConfig , MysqlConfig , PhpConfig , RampConfig } ;
33use serde:: { Deserialize , Serialize } ;
44use std:: io:: Write ;
@@ -38,6 +38,12 @@ impl Default for TomlPhp {
3838/// Load and validate ramp.toml from install_dir.
3939pub fn load_config ( install_dir : & Path ) -> Result < RampConfig , String > {
4040 let paths = InstallPaths :: from_install_dir ( install_dir) ?;
41+
42+ // Reject ramp.toml if it is a symlink — it could redirect config reads/writes
43+ // to a system file, enabling privilege escalation or config escapes.
44+ validate_critical_path ( & paths. config , install_dir, false )
45+ . map_err ( |e| format ! ( "ramp.toml path rejected: {e}" ) ) ?;
46+
4147 let raw = std:: fs:: read_to_string ( & paths. config )
4248 . map_err ( |e| format ! ( "cannot read ramp.toml: {e}" ) ) ?;
4349 let doc: TomlRoot = toml:: from_str ( & raw ) . map_err ( |e| format ! ( "ramp.toml parse error: {e}" ) ) ?;
@@ -54,7 +60,7 @@ pub fn write_default_config(install_dir: &Path) -> Result<(), String> {
5460 r#"install_dir = "{}"
5561
5662[apache]
57- port = 80
63+ port = 8080
5864
5965[mysql]
6066port = 3306
@@ -70,15 +76,20 @@ port = 9000
7076fn validate_and_build ( doc : TomlRoot , install_dir : & Path ) -> Result < RampConfig , String > {
7177 let paths = InstallPaths :: from_install_dir ( install_dir) ?;
7278
73- if doc. apache . port == 0 {
74- return Err ( format ! ( "invalid apache.port: {}" , doc. apache. port) ) ;
75- }
76- if doc. mysql . port == 0 {
77- return Err ( format ! ( "invalid mysql.port: {}" , doc. mysql. port) ) ;
78- }
79- if doc. php . port == 0 {
80- return Err ( format ! ( "invalid php.port: {}" , doc. php. port) ) ;
79+ // Ports must be unprivileged (>= 1024) and non-zero.
80+ // Privileged ports require admin rights on Windows and would cause silent
81+ // startup failures; port 0 is invalid for any bound service.
82+ fn validate_port ( name : & str , port : u16 ) -> Result < ( ) , String > {
83+ if port < 1024 {
84+ return Err ( format ! (
85+ "invalid {name}.port {port}: must be >= 1024 (privileged ports are not allowed)"
86+ ) ) ;
87+ }
88+ Ok ( ( ) )
8189 }
90+ validate_port ( "apache" , doc. apache . port ) ?;
91+ validate_port ( "mysql" , doc. mysql . port ) ?;
92+ validate_port ( "php" , doc. php . port ) ?;
8293 if doc. apache . port == doc. mysql . port {
8394 return Err ( "apache.port and mysql.port must be different" . into ( ) ) ;
8495 }
@@ -191,16 +202,41 @@ port = 3306
191202 & format ! (
192203 r#"install_dir = "{}"
193204[apache]
194- port = 80
205+ port = 8080
195206[mysql]
196- port = 80
207+ port = 8080
197208"# ,
198209 dir. display( ) . to_string( ) . replace( '\\' , "\\ \\ " )
199210 ) ,
200211 ) ;
201212 assert ! ( load_config( dir) . is_err( ) ) ;
202213 }
203214
215+ #[ test]
216+ fn rejects_privileged_port ( ) {
217+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
218+ let dir = tmp. path ( ) ;
219+ write_toml (
220+ dir,
221+ & format ! (
222+ r#"install_dir = "{}"
223+ [apache]
224+ port = 80
225+ [mysql]
226+ port = 3306
227+ [php]
228+ port = 9000
229+ "# ,
230+ dir. display( ) . to_string( ) . replace( '\\' , "\\ \\ " )
231+ ) ,
232+ ) ;
233+ let err = load_config ( dir) . unwrap_err ( ) ;
234+ assert ! (
235+ err. contains( "1024" ) ,
236+ "expected port>=1024 message, got: {err}"
237+ ) ;
238+ }
239+
204240 #[ test]
205241 fn rejects_apache_php_port_clash ( ) {
206242 let tmp = TempDir :: new ( ) . unwrap ( ) ;
@@ -243,4 +279,97 @@ port = 9000
243279 write_default_config ( dir) . unwrap ( ) ;
244280 assert_eq ! ( std:: fs:: read( dir. join( "ramp.toml" ) ) . unwrap( ) , b"original" ) ;
245281 }
282+
283+ #[ test]
284+ fn rejects_malformed_toml ( ) {
285+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
286+ let dir = tmp. path ( ) ;
287+ std:: fs:: write ( dir. join ( "ramp.toml" ) , b"[not valid toml @@@" ) . unwrap ( ) ;
288+ let err = load_config ( dir) . unwrap_err ( ) ;
289+ assert ! (
290+ err. contains( "parse error" ) || err. contains( "TOML" ) || err. contains( "toml" ) ,
291+ "expected parse error message, got: {err}"
292+ ) ;
293+ }
294+
295+ #[ test]
296+ fn rejects_missing_ramp_toml ( ) {
297+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
298+ let err = load_config ( tmp. path ( ) ) . unwrap_err ( ) ;
299+ assert ! (
300+ err. contains( "cannot read" ) || err. contains( "ramp.toml" ) ,
301+ "expected missing file message, got: {err}"
302+ ) ;
303+ }
304+
305+ #[ test]
306+ fn rejects_mysql_php_port_clash ( ) {
307+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
308+ let dir = tmp. path ( ) ;
309+ write_toml (
310+ dir,
311+ & format ! (
312+ r#"install_dir = "{}"
313+ [apache]
314+ port = 8080
315+ [mysql]
316+ port = 9000
317+ [php]
318+ port = 9000
319+ "# ,
320+ dir. display( ) . to_string( ) . replace( '\\' , "\\ \\ " )
321+ ) ,
322+ ) ;
323+ let err = load_config ( dir) . unwrap_err ( ) ;
324+ assert ! (
325+ err. contains( "mysql" ) && err. contains( "php" ) ,
326+ "expected mysql/php clash message, got: {err}"
327+ ) ;
328+ }
329+
330+ #[ test]
331+ fn rejects_privileged_port_mysql ( ) {
332+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
333+ let dir = tmp. path ( ) ;
334+ write_toml (
335+ dir,
336+ & format ! (
337+ r#"install_dir = "{}"
338+ [apache]
339+ port = 8080
340+ [mysql]
341+ port = 1023
342+ [php]
343+ port = 9000
344+ "# ,
345+ dir. display( ) . to_string( ) . replace( '\\' , "\\ \\ " )
346+ ) ,
347+ ) ;
348+ let err = load_config ( dir) . unwrap_err ( ) ;
349+ assert ! (
350+ err. contains( "1024" ) ,
351+ "expected port>=1024 message, got: {err}"
352+ ) ;
353+ }
354+
355+ #[ test]
356+ fn atomic_write_no_tmp_left_on_success ( ) {
357+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
358+ let path = tmp. path ( ) . join ( "out.toml" ) ;
359+ atomic_write ( & path, b"data" ) . unwrap ( ) ;
360+ assert ! ( path. exists( ) ) ;
361+ assert ! ( !path. with_extension( "tmp" ) . exists( ) ) ;
362+ }
363+
364+ #[ test]
365+ fn write_default_config_creates_parseable_toml ( ) {
366+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
367+ let dir = tmp. path ( ) ;
368+ write_default_config ( dir) . unwrap ( ) ;
369+ // The generated default must be loadable — no syntax errors
370+ let cfg = load_config ( dir) . unwrap ( ) ;
371+ assert ! ( cfg. apache. port >= 1024 ) ;
372+ assert ! ( cfg. mysql. port >= 1024 ) ;
373+ assert ! ( cfg. php. port >= 1024 ) ;
374+ }
246375}
0 commit comments