package assetloader import ( "encoding/base64" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "unicode/utf8" ) const ScriptSourcePathMetadataKey = "source_path" var ( embedDirectivePattern = regexp.MustCompile(`(?m)^\s*--\s*@embed:\s*(.+?)\s*$`) embedAttrPattern = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)`) embedVarPattern = regexp.MustCompile(`^:[a-zA-Z_][a-zA-Z0-9_]*$`) ) // ProcessEmbedDirectives expands SQL comments in the form: // // -- @embed: path=... var=:... mode=text|base64 // // Paths are resolved relative to sqlPath. Text mode embeds a quoted UTF-8 SQL // string literal. Base64 mode embeds a quoted base64 literal suitable for // decode(:var, 'base64'). func ProcessEmbedDirectives(sqlPath, sql string) (string, error) { directives := embedDirectivePattern.FindAllStringSubmatch(sql, -1) if len(directives) == 0 { return sql, nil } if sqlPath == "" { return "", fmt.Errorf("sql path is required for embed directives") } result := embedDirectivePattern.ReplaceAllString(sql, "") for i, directive := range directives { literal, placeholder, err := embedDirectiveLiteral(sqlPath, directive[1], i+1) if err != nil { return "", err } if !embedPlaceholderPattern(placeholder).MatchString(result) { return "", fmt.Errorf("%s embed directive %d: placeholder %s not found", sqlPath, i+1, placeholder) } result = replaceEmbedPlaceholder(result, placeholder, literal) } return result, nil } func embedDirectiveLiteral(sqlPath, raw string, directiveNumber int) (literal, placeholder string, err error) { attrs, err := parseEmbedAttrs(raw) if err != nil { return "", "", fmt.Errorf("%s embed directive %d: %w", sqlPath, directiveNumber, err) } pathValue := attrs["path"] varValue := attrs["var"] modeValue := attrs["mode"] if pathValue == "" { return "", "", fmt.Errorf("%s embed directive %d: missing path", sqlPath, directiveNumber) } if !embedVarPattern.MatchString(varValue) { return "", "", fmt.Errorf("%s embed directive %d: var must be a named placeholder like :asset", sqlPath, directiveNumber) } if modeValue != "text" && modeValue != "base64" { return "", "", fmt.Errorf("%s embed directive %d: mode must be text or base64", sqlPath, directiveNumber) } resolved := filepath.Join(filepath.Dir(sqlPath), filepath.Clean(pathValue)) data, err := os.ReadFile(resolved) if err != nil { return "", "", fmt.Errorf("%s embed directive %d: reading %s: %w", sqlPath, directiveNumber, resolved, err) } switch modeValue { case "text": if !utf8.Valid(data) { return "", "", fmt.Errorf("%s embed directive %d: %s is not valid UTF-8", sqlPath, directiveNumber, resolved) } literal, err := sqlStringLiteral(string(data)) if err != nil { return "", "", fmt.Errorf("%s embed directive %d: %w", sqlPath, directiveNumber, err) } return literal, varValue, nil case "base64": literal, err := sqlStringLiteral(base64.StdEncoding.EncodeToString(data)) if err != nil { return "", "", fmt.Errorf("%s embed directive %d: %w", sqlPath, directiveNumber, err) } return literal, varValue, nil default: return "", "", fmt.Errorf("%s embed directive %d: mode must be text or base64", sqlPath, directiveNumber) } } func parseEmbedAttrs(raw string) (map[string]string, error) { attrs := map[string]string{} matches := embedAttrPattern.FindAllStringSubmatchIndex(raw, -1) if len(matches) == 0 { return nil, fmt.Errorf("expected path, var, and mode attributes") } lastEnd := 0 for _, match := range matches { gap := strings.TrimSpace(raw[lastEnd:match[0]]) if gap != "" { return nil, fmt.Errorf("invalid attribute syntax near %q", gap) } key := raw[match[2]:match[3]] value := raw[match[4]:match[5]] if _, exists := attrs[key]; exists { return nil, fmt.Errorf("duplicate attribute %q", key) } unquoted, err := unquoteEmbedValue(value) if err != nil { return nil, fmt.Errorf("invalid %s value: %w", key, err) } attrs[key] = unquoted lastEnd = match[1] } if tail := strings.TrimSpace(raw[lastEnd:]); tail != "" { return nil, fmt.Errorf("invalid attribute syntax near %q", tail) } for key := range attrs { if key != "path" && key != "var" && key != "mode" { return nil, fmt.Errorf("unknown attribute %q", key) } } return attrs, nil } func unquoteEmbedValue(value string) (string, error) { if len(value) < 2 { return value, nil } if value[0] == '"' { return strconv.Unquote(value) } if value[0] == '\'' && value[len(value)-1] == '\'' { return value[1 : len(value)-1], nil } return value, nil } func sqlStringLiteral(value string) (string, error) { if strings.ContainsRune(value, '\x00') { return "", fmt.Errorf("embedded text contains NUL byte") } return "'" + strings.ReplaceAll(value, "'", "''") + "'", nil } func replaceEmbedPlaceholder(sql, placeholder, literal string) string { return embedPlaceholderPattern(placeholder).ReplaceAllString(sql, "${1}"+literal+"${2}") } func embedPlaceholderPattern(placeholder string) *regexp.Regexp { return regexp.MustCompile(`(^|[^a-zA-Z0-9_:])` + regexp.QuoteMeta(placeholder) + `([^a-zA-Z0-9_]|$)`) }