package chatpb // functions to import and export the protobuf // data to and from config files import ( "errors" "fmt" "os" "path/filepath" "time" "go.wit.com/lib/protobuf/bugpb" "go.wit.com/log" ) // write to ~/.config/regex/ unless ENV{REGEX_HOME} is set func (all *Chats) ConfigSave() error { if os.Getenv("REGEX_HOME") == "" { homeDir, _ := os.UserHomeDir() fullpath := filepath.Join(homeDir, ".config/regex") os.Setenv("REGEX_HOME", fullpath) } if all == nil { log.Warn("chatpb all == nil") return errors.New("chatpb.ConfigSave() all == nil") } // --- Start of Fix --- // Create a new, clean Chats object to avoid marshaling a slice with nil entries. cleanChats := NewChats() // Assuming NewChats() initializes the struct correctly. cleanChats.Uuid = all.Uuid cleanChats.Version = all.Version // Loop through the original chats and append only the non-nil ones. for _, chat := range all.GetChats() { if chat != nil { cleanChats.Chats = append(cleanChats.Chats, chat) } else { log.Warn("Found and skipped a nil chat entry during ConfigSave") } } // --- End of Fix --- data, err := cleanChats.Marshal() // Marshal the clean object, not 'all' if err != nil { log.Info("chatpb proto.Marshal() failed len", len(data), err) // The tryValidate logic might be less necessary now but kept for safety. if err := all.tryValidate(); err != nil { return err } else { data, err = cleanChats.Marshal() // Retry with the clean object if err == nil { log.Info("chatpb.ConfigSave() pb.Marshal() worked after validation len", len(cleanChats.Chats), "chats") configWrite("regex.pb", data) return nil } } return err } // --- Backup Logic --- filename := filepath.Join(os.Getenv("REGEX_HOME"), "regex.pb") if _, err := os.Stat(filename); err == nil { // File exists, so back it up. dir := filepath.Dir(filename) timestamp := time.Now().Format("20060102-150405") backupFilename := fmt.Sprintf("regex.%s.pb", timestamp) backupPath := filepath.Join(dir, backupFilename) if err := os.Rename(filename, backupPath); err != nil { log.Warn("Could not backup config file:", err) } } // --- End Backup Logic --- if err := configWrite("regex.pb", data); err != nil { log.Infof("chatpb.ConfigSave() failed len(Chats)=%d bytes=%d", len(cleanChats.Chats), len(data)) return err } configWrite("regex.text", []byte(cleanChats.FormatTEXT())) // log.Infof("chatpb.ConfigSave() worked len(Chats)=%d bytes=%d", len(cleanChats.Chats), len(data)) return nil } func (all *Chats) tryValidate() error { err := bugpb.ValidateProtoUTF8(all) if err != nil { log.Printf("Protobuf UTF-8 validation failed: %v\n", err) } if err := bugpb.SanitizeProtoUTF8(all); err != nil { log.Warn("Sanitation failed:", err) // log.Fatalf("Sanitization failed: %v", err) return err } return nil } // load the regex.pb file. I shouldn't really matter if this // fails. the file should be autogenerated. This is used // locally just for speed func (all *Chats) ConfigLoad() error { if os.Getenv("REGEX_HOME") == "" { homeDir, _ := os.UserHomeDir() fullpath := filepath.Join(homeDir, ".config/regex") os.Setenv("REGEX_HOME", fullpath) } var data []byte var err error cfgname := filepath.Join(os.Getenv("REGEX_HOME"), "regex.pb") if data, err = loadFile(cfgname); err != nil { // something went wrong loading the file // all.sampleConfig() // causes nil panic return err } // this means the regex.pb file exists and was read if len(data) == 0 { // todo: add default data here since it's blank? // might cause nil panic? all.AddRegexComment("I like astronomy") log.Info(errors.New("chatpb.ConfigLoad() regex.pb is empty")) return nil } err = all.Unmarshal(data) test := NewChats() if test.Uuid != all.Uuid { log.Warn("uuids do not match", test.Uuid, all.Uuid) deleteProtobufFile(cfgname) } if test.Version != all.Version { log.Warn("versions do not match", test.Version, all.Version) deleteProtobufFile(cfgname) } // log.Info(cfgname, "protobuf versions and uuid match", all.Uuid, all.Version) return err } func deleteProtobufFile(filename string) { log.Warn("The protobuf file format has changed for", filename) log.Warn("Deleting old file:", filename) log.Warn("This file will be recreated on the next run.") err := os.Remove(filename) if err != nil { log.Warn("failed to remove old protobuf file", "err", err) } } func loadFile(fullname string) ([]byte, error) { data, err := os.ReadFile(fullname) if errors.Is(err, os.ErrNotExist) { // if file does not exist, just return nil. this // will cause ConfigLoad() to try the next config file like "regex.text" // because the user might want to edit the .config by hand return nil, nil } if err != nil { // log.Info("open config file :", err) return nil, err } return data, nil } func configWrite(filename string, data []byte) error { fullname := filepath.Join(os.Getenv("REGEX_HOME"), filename) cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) defer cfgfile.Close() if err != nil { log.Warn("open config file :", err) return err } if filename == "regex.text" { // add header cfgfile.Write([]byte("# this file is automatically re-generated from regex.pb, however,\n")) cfgfile.Write([]byte("# if you want to edit it by hand, you can:\n")) cfgfile.Write([]byte("# stop regex; remove regex.pb; edit regex.text; start regex\n")) cfgfile.Write([]byte("# this will cause the default behavior to fallback to parsing this file for the config\n")) cfgfile.Write([]byte("\n")) cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n")) } cfgfile.Write(data) return nil }