Configuration hardening
This commit is contained in:
@@ -18,12 +18,15 @@ const (
|
||||
DefaultHAKManifest = "haks.json"
|
||||
DefaultHAKArchiveTemplate = "{hak.name}.hak"
|
||||
DefaultSourceJSONPattern = "{resref}.{extension}.json"
|
||||
DefaultValidationProfile = "nwn_module"
|
||||
DefaultScriptsSourceDir = "scripts"
|
||||
DefaultScriptsCache = "{paths.cache}/ncs"
|
||||
DefaultScriptCompilerEnvPath = "SOW_NWN_SCRIPT_COMPILER"
|
||||
DefaultMusicStageRoot = "{paths.cache}/music"
|
||||
DefaultCreditsRoot = "{paths.cache}/credits"
|
||||
DefaultCreditsOverlayFile = "CREDITS.md"
|
||||
DefaultMusicNamingScheme = "nwn_bmu"
|
||||
DefaultMusicOutputExtension = ".bmu"
|
||||
DefaultTopDataBuild = "{paths.build}/topdata"
|
||||
DefaultTopDataCompiled2DADir = "2da"
|
||||
DefaultTopDataCompiledTLK = "sow_tlk.tlk"
|
||||
@@ -60,6 +63,7 @@ type EffectiveConfig struct {
|
||||
Outputs EffectiveOutputConfig `json:"outputs" yaml:"outputs"`
|
||||
Build BuildConfig `json:"build" yaml:"build"`
|
||||
Inventory InventoryConfig `json:"inventory" yaml:"inventory"`
|
||||
Validation ValidationConfig `json:"validation" yaml:"validation"`
|
||||
Scripts EffectiveScriptsConfig `json:"scripts" yaml:"scripts"`
|
||||
Music EffectiveMusicConfig `json:"music" yaml:"music"`
|
||||
TopData EffectiveTopDataConfig `json:"topdata" yaml:"topdata"`
|
||||
@@ -103,19 +107,29 @@ type EffectiveScriptCompilerEnvConfig struct {
|
||||
}
|
||||
|
||||
type EffectiveMusicConfig struct {
|
||||
Prefixes map[string]string `json:"prefixes" yaml:"prefixes"`
|
||||
StageRoot string `json:"stage_root" yaml:"stage_root"`
|
||||
CreditsRoot string `json:"credits_root" yaml:"credits_root"`
|
||||
CreditsOverlay string `json:"credits_overlay" yaml:"credits_overlay"`
|
||||
MaxStemLength int `json:"max_stem_length" yaml:"max_stem_length"`
|
||||
Datasets map[string]EffectiveMusicDataset `json:"datasets,omitempty" yaml:"datasets,omitempty"`
|
||||
Prefixes map[string]string `json:"prefixes" yaml:"prefixes"`
|
||||
Tools MusicToolsConfig `json:"tools" yaml:"tools"`
|
||||
StageRoot string `json:"stage_root" yaml:"stage_root"`
|
||||
CreditsRoot string `json:"credits_root" yaml:"credits_root"`
|
||||
CreditsOverlay string `json:"credits_overlay" yaml:"credits_overlay"`
|
||||
MaxStemLength int `json:"max_stem_length" yaml:"max_stem_length"`
|
||||
NamingScheme string `json:"naming_scheme" yaml:"naming_scheme"`
|
||||
OutputExtension string `json:"output_extension" yaml:"output_extension"`
|
||||
ConvertExtensions []string `json:"convert_extensions" yaml:"convert_extensions"`
|
||||
Datasets map[string]EffectiveMusicDataset `json:"datasets,omitempty" yaml:"datasets,omitempty"`
|
||||
}
|
||||
|
||||
type EffectiveMusicDataset struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Output string `json:"output" yaml:"output"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme" yaml:"naming_scheme"`
|
||||
OutputExtension string `json:"output_extension" yaml:"output_extension"`
|
||||
MaxStemLength int `json:"max_stem_length" yaml:"max_stem_length"`
|
||||
PackageMode string `json:"package_mode" yaml:"package_mode"`
|
||||
ConvertExtensions []string `json:"convert_extensions" yaml:"convert_extensions"`
|
||||
}
|
||||
|
||||
type EffectiveTopDataConfig struct {
|
||||
@@ -177,6 +191,11 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
AssetExtensions: defaultStringSlice(p.Config.Inventory.AssetExtensions, AssetExtensions),
|
||||
SourceJSONPattern: defaultString(p.Config.Inventory.SourceJSONPattern, DefaultSourceJSONPattern),
|
||||
}
|
||||
validation := ValidationConfig{
|
||||
Profile: defaultString(p.Config.Validation.Profile, DefaultValidationProfile),
|
||||
BuiltinScriptPrefixes: normalizeLowerStringSlice(defaultStringSlice(p.Config.Validation.BuiltinScriptPrefixes, DefaultBuiltinScriptPrefixes())),
|
||||
RequiredFields: defaultRequiredFields(p.Config.Validation.RequiredFields),
|
||||
}
|
||||
scripts := EffectiveScriptsConfig{
|
||||
SourceDir: defaultString(p.Config.Scripts.SourceDir, DefaultScriptsSourceDir),
|
||||
Cache: expandPathTemplate(defaultString(p.Config.Scripts.Cache, DefaultScriptsCache), paths),
|
||||
@@ -221,6 +240,9 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
musicCreditsRoot := expandPathTemplate(DefaultCreditsRoot, paths)
|
||||
musicCreditsOverlay := DefaultCreditsOverlayFile
|
||||
musicMaxStemLength := 16
|
||||
musicNamingScheme := DefaultMusicNamingScheme
|
||||
musicOutputExtension := DefaultMusicOutputExtension
|
||||
musicConvertExtensions := []string{".mp3", ".ogg"}
|
||||
if d := p.Config.Music.Defaults; d != nil {
|
||||
if d.StageRoot != "" {
|
||||
musicStageRoot = expandPathTemplate(d.StageRoot, paths)
|
||||
@@ -234,6 +256,15 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
if d.MaxStemLength != nil {
|
||||
musicMaxStemLength = *d.MaxStemLength
|
||||
}
|
||||
if d.NamingScheme != "" {
|
||||
musicNamingScheme = d.NamingScheme
|
||||
}
|
||||
if d.OutputExtension != "" {
|
||||
musicOutputExtension = normalizeExtension(d.OutputExtension)
|
||||
}
|
||||
if len(d.ConvertExtensions) > 0 {
|
||||
musicConvertExtensions = normalizeExtensionSlice(d.ConvertExtensions)
|
||||
}
|
||||
}
|
||||
musicPrefixes := cloneStringMap(p.Config.Music.Prefixes)
|
||||
if len(musicPrefixes) == 0 {
|
||||
@@ -254,11 +285,30 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
if dsCreditsRoot == "" {
|
||||
dsCreditsRoot = musicCreditsRoot
|
||||
}
|
||||
dsOutput := strings.TrimSpace(ds.Output)
|
||||
if dsOutput == "" {
|
||||
dsOutput = ds.Source
|
||||
}
|
||||
dsNamingScheme := defaultString(ds.NamingScheme, musicNamingScheme)
|
||||
dsOutputExtension := musicOutputExtension
|
||||
if ds.OutputExtension != "" {
|
||||
dsOutputExtension = normalizeExtension(ds.OutputExtension)
|
||||
}
|
||||
dsConvertExtensions := musicConvertExtensions
|
||||
if len(ds.ConvertExtensions) > 0 {
|
||||
dsConvertExtensions = normalizeExtensionSlice(ds.ConvertExtensions)
|
||||
}
|
||||
effectiveDatasets[id] = EffectiveMusicDataset{
|
||||
Source: ds.Source,
|
||||
Prefix: ds.Prefix,
|
||||
StageRoot: dsStageRoot,
|
||||
CreditsRoot: dsCreditsRoot,
|
||||
Source: ds.Source,
|
||||
Output: dsOutput,
|
||||
Prefix: ds.Prefix,
|
||||
StageRoot: dsStageRoot,
|
||||
CreditsRoot: dsCreditsRoot,
|
||||
NamingScheme: dsNamingScheme,
|
||||
OutputExtension: dsOutputExtension,
|
||||
MaxStemLength: musicMaxStemLength,
|
||||
PackageMode: defaultString(ds.PackageMode, "hak_asset"),
|
||||
ConvertExtensions: dsConvertExtensions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,14 +319,19 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
Outputs: outputs,
|
||||
Build: p.Config.Build,
|
||||
Inventory: inventory,
|
||||
Validation: validation,
|
||||
Scripts: scripts,
|
||||
Music: EffectiveMusicConfig{
|
||||
Prefixes: musicPrefixes,
|
||||
StageRoot: musicStageRoot,
|
||||
CreditsRoot: musicCreditsRoot,
|
||||
CreditsOverlay: musicCreditsOverlay,
|
||||
MaxStemLength: musicMaxStemLength,
|
||||
Datasets: effectiveDatasets,
|
||||
Prefixes: musicPrefixes,
|
||||
Tools: p.Config.Music.Tools,
|
||||
StageRoot: musicStageRoot,
|
||||
CreditsRoot: musicCreditsRoot,
|
||||
CreditsOverlay: musicCreditsOverlay,
|
||||
MaxStemLength: musicMaxStemLength,
|
||||
NamingScheme: musicNamingScheme,
|
||||
OutputExtension: musicOutputExtension,
|
||||
ConvertExtensions: musicConvertExtensions,
|
||||
Datasets: effectiveDatasets,
|
||||
},
|
||||
TopData: top,
|
||||
Extract: extract,
|
||||
@@ -353,6 +408,9 @@ func markMissingDefaults(provenance ConfigProvenance) {
|
||||
"inventory.source_extensions": "NWN source resource extensions",
|
||||
"inventory.asset_extensions": "NWN asset resource extensions",
|
||||
"inventory.source_json_pattern": DefaultSourceJSONPattern,
|
||||
"validation.profile": DefaultValidationProfile,
|
||||
"validation.builtin_script_prefixes": "NWN built-in script prefixes",
|
||||
"validation.required_fields": "NWN required GFF fields",
|
||||
"scripts.source_dir": DefaultScriptsSourceDir,
|
||||
"scripts.cache": DefaultScriptsCache,
|
||||
"scripts.compiler.env.path": DefaultScriptCompilerEnvPath,
|
||||
@@ -361,6 +419,9 @@ func markMissingDefaults(provenance ConfigProvenance) {
|
||||
"music.credits_root": DefaultCreditsRoot,
|
||||
"music.credits_overlay": DefaultCreditsOverlayFile,
|
||||
"music.max_stem_length": "16",
|
||||
"music.naming_scheme": DefaultMusicNamingScheme,
|
||||
"music.output_extension": DefaultMusicOutputExtension,
|
||||
"music.convert_extensions": ".mp3, .ogg",
|
||||
"topdata.build": DefaultTopDataBuild,
|
||||
"topdata.compiled_2da_dir": DefaultTopDataCompiled2DADir,
|
||||
"topdata.compiled_tlk": DefaultTopDataCompiledTLK,
|
||||
@@ -413,6 +474,21 @@ func defaultStringSlice(value, fallback []string) []string {
|
||||
return slices.Clone(value)
|
||||
}
|
||||
|
||||
func defaultRequiredFields(configured map[string][]string) map[string][]string {
|
||||
if len(configured) > 0 {
|
||||
return cloneStringSliceMap(normalizeRequiredFields(configured))
|
||||
}
|
||||
return DefaultRequiredFields()
|
||||
}
|
||||
|
||||
func cloneStringSliceMap(input map[string][]string) map[string][]string {
|
||||
out := make(map[string][]string, len(input))
|
||||
for key, values := range input {
|
||||
out[key] = slices.Clone(values)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStringMap(input map[string]string) map[string]string {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
|
||||
+241
-20
@@ -33,6 +33,36 @@ var AssetExtensions = []string{
|
||||
".2da", ".bik", ".bmp", ".bmu", ".dds", ".dwk", ".gr2", ".itp", ".jpg", ".lod", ".lyt", ".mdb", ".mdl", ".mdx", ".mp3", ".mtr", ".ogg", ".plt", ".png", ".pwk", ".set", ".shd", ".tga", ".txi", ".uti", ".vis", ".wav", ".wlk", ".wok", ".xml",
|
||||
}
|
||||
|
||||
var BuiltinScriptPrefixes = []string{
|
||||
"nw_",
|
||||
"x0_",
|
||||
"x1_",
|
||||
"x2_",
|
||||
"x3_",
|
||||
"ga_",
|
||||
"gc_",
|
||||
"gen_",
|
||||
"gui_",
|
||||
"nwg_",
|
||||
"ta_",
|
||||
}
|
||||
|
||||
var RequiredFields = map[string][]string{
|
||||
"ifo": {"Mod_Name"},
|
||||
}
|
||||
|
||||
func DefaultBuiltinScriptPrefixes() []string {
|
||||
return slices.Clone(BuiltinScriptPrefixes)
|
||||
}
|
||||
|
||||
func DefaultRequiredFields() map[string][]string {
|
||||
out := make(map[string][]string, len(RequiredFields))
|
||||
for extension, fields := range RequiredFields {
|
||||
out[extension] = slices.Clone(fields)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Root string
|
||||
Config Config
|
||||
@@ -49,17 +79,18 @@ type ConfigSource struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Module ModuleConfig `json:"module" yaml:"module"`
|
||||
Paths PathConfig `json:"paths" yaml:"paths"`
|
||||
Outputs OutputConfig `json:"outputs" yaml:"outputs"`
|
||||
Build BuildConfig `json:"build" yaml:"build"`
|
||||
HAKs []HAKConfig `json:"haks" yaml:"haks"`
|
||||
Inventory InventoryConfig `json:"inventory" yaml:"inventory"`
|
||||
Scripts ScriptsConfig `json:"scripts" yaml:"scripts"`
|
||||
Music MusicConfig `json:"music" yaml:"music"`
|
||||
TopData TopDataConfig `json:"topdata" yaml:"topdata"`
|
||||
Extract ExtractConfig `json:"extract" yaml:"extract"`
|
||||
Autogen AutogenConfig `json:"autogen" yaml:"autogen"`
|
||||
Module ModuleConfig `json:"module" yaml:"module"`
|
||||
Paths PathConfig `json:"paths" yaml:"paths"`
|
||||
Outputs OutputConfig `json:"outputs" yaml:"outputs"`
|
||||
Build BuildConfig `json:"build" yaml:"build"`
|
||||
HAKs []HAKConfig `json:"haks" yaml:"haks"`
|
||||
Inventory InventoryConfig `json:"inventory" yaml:"inventory"`
|
||||
Validation ValidationConfig `json:"validation" yaml:"validation"`
|
||||
Scripts ScriptsConfig `json:"scripts" yaml:"scripts"`
|
||||
Music MusicConfig `json:"music" yaml:"music"`
|
||||
TopData TopDataConfig `json:"topdata" yaml:"topdata"`
|
||||
Extract ExtractConfig `json:"extract" yaml:"extract"`
|
||||
Autogen AutogenConfig `json:"autogen" yaml:"autogen"`
|
||||
}
|
||||
|
||||
type ModuleConfig struct {
|
||||
@@ -93,6 +124,12 @@ type InventoryConfig struct {
|
||||
SourceJSONPattern string `json:"source_json_pattern" yaml:"source_json_pattern"`
|
||||
}
|
||||
|
||||
type ValidationConfig struct {
|
||||
Profile string `json:"profile" yaml:"profile"`
|
||||
BuiltinScriptPrefixes []string `json:"builtin_script_prefixes" yaml:"builtin_script_prefixes"`
|
||||
RequiredFields map[string][]string `json:"required_fields" yaml:"required_fields"`
|
||||
}
|
||||
|
||||
type ScriptsConfig struct {
|
||||
SourceDir string `json:"source_dir" yaml:"source_dir"`
|
||||
Cache string `json:"cache" yaml:"cache"`
|
||||
@@ -121,23 +158,37 @@ type HAKConfig struct {
|
||||
}
|
||||
|
||||
type MusicConfig struct {
|
||||
Prefixes map[string]string `json:"prefixes,omitempty" yaml:"prefixes,omitempty"`
|
||||
Prefixes map[string]string `json:"prefixes,omitempty" yaml:"prefixes,omitempty"`
|
||||
Tools MusicToolsConfig `json:"tools,omitempty" yaml:"tools,omitempty"`
|
||||
Defaults *MusicDefaultsConfig `json:"defaults,omitempty" yaml:"defaults,omitempty"`
|
||||
Datasets map[string]MusicDatasetConfig `json:"datasets,omitempty" yaml:"datasets,omitempty"`
|
||||
}
|
||||
|
||||
type MusicToolsConfig struct {
|
||||
FFmpeg string `json:"ffmpeg,omitempty" yaml:"ffmpeg,omitempty"`
|
||||
FFprobe string `json:"ffprobe,omitempty" yaml:"ffprobe,omitempty"`
|
||||
}
|
||||
|
||||
type MusicDefaultsConfig struct {
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
CreditsOverlay string `json:"credits_overlay,omitempty" yaml:"credits_overlay,omitempty"`
|
||||
MaxStemLength *int `json:"max_stem_length,omitempty" yaml:"max_stem_length,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
CreditsOverlay string `json:"credits_overlay,omitempty" yaml:"credits_overlay,omitempty"`
|
||||
MaxStemLength *int `json:"max_stem_length,omitempty" yaml:"max_stem_length,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme,omitempty" yaml:"naming_scheme,omitempty"`
|
||||
OutputExtension string `json:"output_extension,omitempty" yaml:"output_extension,omitempty"`
|
||||
ConvertExtensions []string `json:"convert_extensions,omitempty" yaml:"convert_extensions,omitempty"`
|
||||
}
|
||||
|
||||
type MusicDatasetConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme,omitempty" yaml:"naming_scheme,omitempty"`
|
||||
OutputExtension string `json:"output_extension,omitempty" yaml:"output_extension,omitempty"`
|
||||
PackageMode string `json:"package_mode,omitempty" yaml:"package_mode,omitempty"`
|
||||
ConvertExtensions []string `json:"convert_extensions,omitempty" yaml:"convert_extensions,omitempty"`
|
||||
}
|
||||
|
||||
type TopDataConfig struct {
|
||||
@@ -387,6 +438,7 @@ func (p *Project) ValidateLayout() error {
|
||||
"outputs.module_archive": p.Config.Outputs.ModuleArchive,
|
||||
"outputs.hak_manifest": p.Config.Outputs.HAKManifest,
|
||||
"outputs.hak_archive": p.Config.Outputs.HAKArchive,
|
||||
"validation.profile": p.Config.Validation.Profile,
|
||||
"scripts.source_dir": p.Config.Scripts.SourceDir,
|
||||
"scripts.cache": p.Config.Scripts.Cache,
|
||||
"scripts.compiler.path": p.Config.Scripts.Compiler.Path,
|
||||
@@ -415,6 +467,7 @@ func (p *Project) ValidateLayout() error {
|
||||
failures = append(failures, fmt.Errorf("%s must not contain NUL bytes", field))
|
||||
}
|
||||
}
|
||||
failures = append(failures, validateValidationConfig(p.Config.Validation)...)
|
||||
effective := p.EffectiveConfig()
|
||||
failures = append(failures, validateRelativePath("paths.cache", effective.Paths.Cache)...)
|
||||
failures = append(failures, validateRelativePath("outputs.module_archive", effective.Outputs.ModuleArchive)...)
|
||||
@@ -815,6 +868,25 @@ func (p *Project) CreditsDir() string {
|
||||
return p.rootPath(p.EffectiveConfig().Music.CreditsRoot)
|
||||
}
|
||||
|
||||
func (p *Project) MusicToolPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(p.Root, filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
func (p *Project) MusicFFmpegPath() string {
|
||||
return p.MusicToolPath(p.EffectiveConfig().Music.Tools.FFmpeg)
|
||||
}
|
||||
|
||||
func (p *Project) MusicFFprobePath() string {
|
||||
return p.MusicToolPath(p.EffectiveConfig().Music.Tools.FFprobe)
|
||||
}
|
||||
|
||||
func (p *Project) AutogenCacheDir() string {
|
||||
return p.rootPath(p.EffectiveConfig().Autogen.Cache.Root)
|
||||
}
|
||||
@@ -877,6 +949,9 @@ func defaultConfig() Config {
|
||||
func normalizeConfig(cfg *Config) {
|
||||
cfg.Inventory.SourceExtensions = normalizeExtensionSlice(cfg.Inventory.SourceExtensions)
|
||||
cfg.Inventory.AssetExtensions = normalizeExtensionSlice(cfg.Inventory.AssetExtensions)
|
||||
cfg.Validation.BuiltinScriptPrefixes = normalizeLowerStringSlice(cfg.Validation.BuiltinScriptPrefixes)
|
||||
cfg.Validation.Profile = strings.ToLower(strings.TrimSpace(cfg.Validation.Profile))
|
||||
cfg.Validation.RequiredFields = normalizeRequiredFields(cfg.Validation.RequiredFields)
|
||||
for i := range cfg.HAKs {
|
||||
cfg.HAKs[i].Include = normalizeStringSlice(cfg.HAKs[i].Include)
|
||||
}
|
||||
@@ -891,6 +966,10 @@ func normalizeConfig(cfg *Config) {
|
||||
}
|
||||
|
||||
func normalizeMusicConfig(music *MusicConfig) {
|
||||
if music.Defaults != nil {
|
||||
music.Defaults.ConvertExtensions = normalizeExtensionSlice(music.Defaults.ConvertExtensions)
|
||||
music.Defaults.OutputExtension = normalizeExtension(music.Defaults.OutputExtension)
|
||||
}
|
||||
if len(music.Prefixes) > 0 && len(music.Datasets) == 0 {
|
||||
datasets := make(map[string]MusicDatasetConfig, len(music.Prefixes))
|
||||
for dir, prefix := range music.Prefixes {
|
||||
@@ -906,6 +985,13 @@ func normalizeMusicConfig(music *MusicConfig) {
|
||||
}
|
||||
music.Datasets = datasets
|
||||
}
|
||||
for id, ds := range music.Datasets {
|
||||
ds.Source = trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Source)))
|
||||
ds.Output = trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Output)))
|
||||
ds.OutputExtension = normalizeExtension(ds.OutputExtension)
|
||||
ds.ConvertExtensions = normalizeExtensionSlice(ds.ConvertExtensions)
|
||||
music.Datasets[id] = ds
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringSlice(input []string) []string {
|
||||
@@ -935,6 +1021,108 @@ func normalizeExtensionSlice(input []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeExtension(input string) string {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(input))
|
||||
if trimmed != "" && !strings.HasPrefix(trimmed, ".") {
|
||||
trimmed = "." + trimmed
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func normalizeLowerStringSlice(input []string) []string {
|
||||
if len(input) == 0 {
|
||||
return input
|
||||
}
|
||||
out := make([]string, 0, len(input))
|
||||
for _, value := range input {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(value))
|
||||
if trimmed != "" {
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeRequiredFields(input map[string][]string) map[string][]string {
|
||||
if len(input) == 0 {
|
||||
return input
|
||||
}
|
||||
out := make(map[string][]string, len(input))
|
||||
for extension, labels := range input {
|
||||
key := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(extension)), ".")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
normalizedLabels := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
label = strings.TrimSpace(label)
|
||||
if label != "" {
|
||||
normalizedLabels = append(normalizedLabels, label)
|
||||
}
|
||||
}
|
||||
slices.Sort(normalizedLabels)
|
||||
out[key] = normalizedLabels
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateValidationConfig(cfg ValidationConfig) []error {
|
||||
var failures []error
|
||||
switch strings.TrimSpace(cfg.Profile) {
|
||||
case "", DefaultValidationProfile:
|
||||
default:
|
||||
failures = append(failures, fmt.Errorf("validation.profile %q is not supported", cfg.Profile))
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for index, prefix := range cfg.BuiltinScriptPrefixes {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
failures = append(failures, fmt.Errorf("validation.builtin_script_prefixes[%d] must not be empty", index))
|
||||
continue
|
||||
}
|
||||
if strings.Contains(prefix, "\x00") {
|
||||
failures = append(failures, fmt.Errorf("validation.builtin_script_prefixes[%d] must not contain NUL bytes", index))
|
||||
}
|
||||
normalized := strings.ToLower(prefix)
|
||||
if _, exists := seen[normalized]; exists {
|
||||
failures = append(failures, fmt.Errorf("validation.builtin_script_prefixes[%d] %q is duplicated", index, prefix))
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
}
|
||||
seenExtensions := map[string]struct{}{}
|
||||
for extension, labels := range cfg.RequiredFields {
|
||||
normalizedExtension := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(extension)), ".")
|
||||
if normalizedExtension == "" {
|
||||
failures = append(failures, errors.New("validation.required_fields contains an empty extension"))
|
||||
continue
|
||||
}
|
||||
if strings.Contains(normalizedExtension, "\x00") {
|
||||
failures = append(failures, fmt.Errorf("validation.required_fields[%q] must not contain NUL bytes", extension))
|
||||
}
|
||||
if _, exists := seenExtensions[normalizedExtension]; exists {
|
||||
failures = append(failures, fmt.Errorf("validation.required_fields[%q] is duplicated after normalization", extension))
|
||||
}
|
||||
seenExtensions[normalizedExtension] = struct{}{}
|
||||
seenLabels := map[string]struct{}{}
|
||||
for index, label := range labels {
|
||||
label = strings.TrimSpace(label)
|
||||
if label == "" {
|
||||
failures = append(failures, fmt.Errorf("validation.required_fields[%q][%d] must not be empty", extension, index))
|
||||
continue
|
||||
}
|
||||
if strings.Contains(label, "\x00") {
|
||||
failures = append(failures, fmt.Errorf("validation.required_fields[%q][%d] must not contain NUL bytes", extension, index))
|
||||
}
|
||||
if _, exists := seenLabels[label]; exists {
|
||||
failures = append(failures, fmt.Errorf("validation.required_fields[%q][%d] %q is duplicated", extension, index, label))
|
||||
}
|
||||
seenLabels[label] = struct{}{}
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func validateAutogenConfig(cfg AutogenConfig) []error {
|
||||
var failures []error
|
||||
producerIDs := map[string]struct{}{}
|
||||
@@ -1014,6 +1202,17 @@ func validateAutogenConfig(cfg AutogenConfig) []error {
|
||||
|
||||
func validateMusicConfig(cfg MusicConfig) []error {
|
||||
var failures []error
|
||||
if cfg.Defaults != nil {
|
||||
if cfg.Defaults.MaxStemLength != nil && *cfg.Defaults.MaxStemLength < 3 {
|
||||
failures = append(failures, errors.New("music.defaults.max_stem_length must be at least 3"))
|
||||
}
|
||||
if cfg.Defaults.NamingScheme != "" && cfg.Defaults.NamingScheme != "nwn_bmu" && cfg.Defaults.NamingScheme != "passthrough" {
|
||||
failures = append(failures, fmt.Errorf("music.defaults.naming_scheme %q is not supported", cfg.Defaults.NamingScheme))
|
||||
}
|
||||
if cfg.Defaults.OutputExtension != "" && !strings.HasPrefix(cfg.Defaults.OutputExtension, ".") {
|
||||
failures = append(failures, errors.New("music.defaults.output_extension must start with ."))
|
||||
}
|
||||
}
|
||||
normalizedRoots := map[string]string{}
|
||||
for root, prefix := range cfg.Prefixes {
|
||||
normalizedRoot := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(root)))
|
||||
@@ -1033,19 +1232,41 @@ func validateMusicConfig(cfg MusicConfig) []error {
|
||||
for id, ds := range cfg.Datasets {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
failures = append(failures, errors.New("music.datasets contains an empty key"))
|
||||
} else if strings.Contains(id, "/") || strings.Contains(id, "\\") {
|
||||
failures = append(failures, fmt.Errorf("music.datasets id %q must not contain path separators", id))
|
||||
}
|
||||
source := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Source)))
|
||||
if source == "" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source is required", id))
|
||||
} else if pathEscapesRoot(source) {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source must not escape project root", id))
|
||||
} else if prior, exists := seenSources[source]; exists {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source %q conflicts with dataset %q", id, ds.Source, prior))
|
||||
} else {
|
||||
seenSources[source] = id
|
||||
}
|
||||
output := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Output)))
|
||||
if output != "" && pathEscapesRoot(output) {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].output must not escape project root", id))
|
||||
}
|
||||
if ds.NamingScheme != "" && ds.NamingScheme != "nwn_bmu" && ds.NamingScheme != "passthrough" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].naming_scheme %q is not supported", id, ds.NamingScheme))
|
||||
}
|
||||
if ds.PackageMode != "" && ds.PackageMode != "hak_asset" && ds.PackageMode != "none" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].package_mode %q is not supported", id, ds.PackageMode))
|
||||
}
|
||||
if ds.OutputExtension != "" && !strings.HasPrefix(ds.OutputExtension, ".") {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].output_extension must start with .", id))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func pathEscapesRoot(path string) bool {
|
||||
clean := filepath.Clean(filepath.FromSlash(path))
|
||||
return clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func validateGlobList(field string, patterns []string) []error {
|
||||
var failures []error
|
||||
seen := map[string]struct{}{}
|
||||
|
||||
@@ -69,6 +69,12 @@ paths:
|
||||
if got, want := effective.TopData.PackageHAK, "sow_top.hak"; got != want {
|
||||
t.Fatalf("expected default topdata HAK %q, got %q", want, got)
|
||||
}
|
||||
if got, want := strings.Join(effective.Validation.BuiltinScriptPrefixes, ","), "ga_,gc_,gen_,gui_,nw_,nwg_,ta_,x0_,x1_,x2_,x3_"; got != want {
|
||||
t.Fatalf("expected default built-in script prefixes %q, got %q", want, got)
|
||||
}
|
||||
if got := strings.Join(effective.Validation.RequiredFields["ifo"], ","); got != "Mod_Name" {
|
||||
t.Fatalf("expected default IFO required fields, got %#v", effective.Validation.RequiredFields)
|
||||
}
|
||||
if prov := effective.Provenance["paths.build"]; prov.Source != "toolkit default" {
|
||||
t.Fatalf("expected paths.build toolkit default provenance, got %#v", prov)
|
||||
}
|
||||
@@ -77,6 +83,37 @@ paths:
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigHonorsValidationConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
source: src
|
||||
validation:
|
||||
builtin_script_prefixes:
|
||||
- custom_
|
||||
- NW_
|
||||
required_fields:
|
||||
ifo:
|
||||
- Mod_Name
|
||||
- Mod_Hak
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
got := strings.Join(proj.EffectiveConfig().Validation.BuiltinScriptPrefixes, ",")
|
||||
if want := "custom_,nw_"; got != want {
|
||||
t.Fatalf("expected configured validation prefixes %q, got %q", want, got)
|
||||
}
|
||||
if got := strings.Join(proj.EffectiveConfig().Validation.RequiredFields["ifo"], ","); got != "Mod_Hak,Mod_Name" {
|
||||
t.Fatalf("expected configured required fields, got %#v", proj.EffectiveConfig().Validation.RequiredFields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigHonorsConfiguredOutputAndCacheFields(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
|
||||
Reference in New Issue
Block a user