89 lines
2.0 KiB
Go
89 lines
2.0 KiB
Go
package research
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// Matches Paper Atlas repair 4e62710's nul-soh-v1 protocol: NUL -> SOH/SOH and literal
|
|
// SOH -> SOH/STX. All other bytes retain their original binary ordering.
|
|
const textCodec = "nul-soh-v1"
|
|
|
|
func encodedSchema(annotation sql.NullString) (bool, error) {
|
|
const prefix = "paper-atlas:text-codec="
|
|
if !annotation.Valid || !strings.HasPrefix(annotation.String, prefix) {
|
|
return false, nil
|
|
}
|
|
if strings.TrimPrefix(annotation.String, prefix) != textCodec {
|
|
return false, errors.New("unsupported research text codec")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func encodeText(value string) string {
|
|
return strings.ReplaceAll(strings.ReplaceAll(value, "\x01", "\x01\x02"), "\x00", "\x01\x01")
|
|
}
|
|
|
|
func decodeText(value string) (string, error) {
|
|
if !strings.ContainsRune(value, '\x01') {
|
|
return value, nil
|
|
}
|
|
var result strings.Builder
|
|
for {
|
|
position := strings.IndexByte(value, 1)
|
|
if position < 0 {
|
|
result.WriteString(value)
|
|
return result.String(), nil
|
|
}
|
|
result.WriteString(value[:position])
|
|
if position+1 >= len(value) {
|
|
return "", errors.New("invalid research text escape")
|
|
}
|
|
switch value[position+1] {
|
|
case 1:
|
|
result.WriteByte(0)
|
|
case 2:
|
|
result.WriteByte(1)
|
|
default:
|
|
return "", errors.New("invalid research text escape")
|
|
}
|
|
value = value[position+2:]
|
|
}
|
|
}
|
|
|
|
type textColumn struct {
|
|
encoded bool
|
|
target any
|
|
}
|
|
|
|
func (column textColumn) Scan(source any) error {
|
|
var value sql.NullString
|
|
if err := value.Scan(source); err != nil {
|
|
return err
|
|
}
|
|
if value.Valid && column.encoded {
|
|
var err error
|
|
value.String, err = decodeText(value.String)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
switch target := column.target.(type) {
|
|
case *string:
|
|
if !value.Valid {
|
|
return errors.New("unexpected null research text")
|
|
}
|
|
*target = value.String
|
|
case *sql.NullString:
|
|
*target = value
|
|
default:
|
|
return errors.New("unsupported research text destination")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) text(target any) sql.Scanner {
|
|
return textColumn{encoded: r.encoded, target: target}
|
|
}
|