88 "log/slog"
99 "net/http"
1010 "net/url"
11+ "os"
12+ "path/filepath"
1113 "regexp"
1214 "slices"
1315 "strconv"
@@ -24,6 +26,7 @@ import (
2426 "github.com/osbuild/image-builder-crc/internal/common"
2527 "github.com/osbuild/image-builder-crc/internal/db"
2628 "github.com/osbuild/images/pkg/crypt"
29+ "github.com/osbuild/images/pkg/customizations/fsnode"
2730)
2831
2932var (
@@ -294,6 +297,149 @@ func validateBlueprintRequest(ctx echo.Context, blueprintRequest *CreateBlueprin
294297 }
295298 }
296299
300+ // Validate Files using images library fsnode validation
301+ if blueprintRequest .Customizations .Files != nil {
302+ for _ , file := range * blueprintRequest .Customizations .Files {
303+ // Convert API types to fsnode types for validation
304+ var mode * os.FileMode
305+ if file .Mode != nil {
306+ // Parse octal mode string to os.FileMode
307+ if modeVal , err := strconv .ParseUint (* file .Mode , 8 , 32 ); err == nil {
308+ m := os .FileMode (modeVal )
309+ mode = & m
310+ }
311+ }
312+
313+ var user interface {}
314+ if file .User != nil {
315+ // Handle union type - could be string or int64
316+ if userStr , err := file .User .AsFileUser0 (); err == nil {
317+ user = userStr
318+ } else if userInt , err := file .User .AsFileUser1 (); err == nil {
319+ user = userInt
320+ }
321+ }
322+
323+ var group interface {}
324+ if file .Group != nil {
325+ // Handle union type - could be string or int64
326+ if groupStr , err := file .Group .AsFileGroup0 (); err == nil {
327+ group = groupStr
328+ } else if groupInt , err := file .Group .AsFileGroup1 (); err == nil {
329+ group = groupInt
330+ }
331+ }
332+
333+ var data []byte
334+ if file .Data != nil {
335+ data = []byte (* file .Data )
336+ }
337+
338+ // Use fsnode.NewFile for validation - this handles all path, mode, user, group validation
339+ _ , err := fsnode .NewFile (file .Path , mode , user , group , data )
340+ if err != nil {
341+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
342+ Errors : []HTTPError {{
343+ Title : "Invalid file customization" ,
344+ Detail : fmt .Sprintf ("file %q: %s" , file .Path , err .Error ()),
345+ }},
346+ })
347+ }
348+ }
349+ }
350+
351+ // Validate Directories using images library fsnode validation
352+ if blueprintRequest .Customizations .Directories != nil {
353+ for _ , dir := range * blueprintRequest .Customizations .Directories {
354+ // Convert API types to fsnode types for validation
355+ var mode * os.FileMode
356+ if dir .Mode != nil {
357+ // Parse octal mode string to os.FileMode
358+ if modeVal , err := strconv .ParseUint (* dir .Mode , 8 , 32 ); err == nil {
359+ m := os .FileMode (modeVal )
360+ mode = & m
361+ }
362+ }
363+
364+ var user interface {}
365+ if dir .User != nil {
366+ // Handle union type - could be string or int64
367+ if userStr , err := dir .User .AsDirectoryUser0 (); err == nil {
368+ user = userStr
369+ } else if userInt , err := dir .User .AsDirectoryUser1 (); err == nil {
370+ user = userInt
371+ }
372+ }
373+
374+ var group interface {}
375+ if dir .Group != nil {
376+ // Handle union type - could be string or int64
377+ if groupStr , err := dir .Group .AsDirectoryGroup0 (); err == nil {
378+ group = groupStr
379+ } else if groupInt , err := dir .Group .AsDirectoryGroup1 (); err == nil {
380+ group = groupInt
381+ }
382+ }
383+
384+ ensureParents := false
385+ if dir .EnsureParents != nil {
386+ ensureParents = * dir .EnsureParents
387+ }
388+
389+ // Use fsnode.NewDirectory for validation - this handles all path, mode, user, group validation
390+ _ , err := fsnode .NewDirectory (dir .Path , mode , user , group , ensureParents )
391+ if err != nil {
392+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
393+ Errors : []HTTPError {{
394+ Title : "Invalid directory customization" ,
395+ Detail : fmt .Sprintf ("directory %q: %s" , dir .Path , err .Error ()),
396+ }},
397+ })
398+ }
399+ }
400+ }
401+
402+ // Validate Filesystem using basic path validation patterns from fsnode library
403+ if blueprintRequest .Customizations .Filesystem != nil {
404+ for _ , fs := range * blueprintRequest .Customizations .Filesystem {
405+ // Use the same path validation logic as fsnode (following library patterns)
406+ if fs .Mountpoint == "" {
407+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
408+ Errors : []HTTPError {{
409+ Title : "Invalid filesystem customization" ,
410+ Detail : "mountpoint must not be empty" ,
411+ }},
412+ })
413+ }
414+ if fs .Mountpoint [0 ] != '/' {
415+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
416+ Errors : []HTTPError {{
417+ Title : "Invalid filesystem customization" ,
418+ Detail : fmt .Sprintf ("mountpoint %q must be absolute" , fs .Mountpoint ),
419+ }},
420+ })
421+ }
422+ if fs .Mountpoint != filepath .Clean (fs .Mountpoint ) {
423+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
424+ Errors : []HTTPError {{
425+ Title : "Invalid filesystem customization" ,
426+ Detail : fmt .Sprintf ("mountpoint %q must be canonical" , fs .Mountpoint ),
427+ }},
428+ })
429+ }
430+
431+ // Validate minimum size is reasonable
432+ if fs .MinSize > 0 && fs .MinSize < 1024 * 1024 { // 1MB minimum
433+ return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
434+ Errors : []HTTPError {{
435+ Title : "Invalid filesystem customization" ,
436+ Detail : fmt .Sprintf ("mountpoint %q minimum size must be at least 1MB" , fs .Mountpoint ),
437+ }},
438+ })
439+ }
440+ }
441+ }
442+
297443 return nil
298444}
299445
0 commit comments