package sudoku import ( crypto_rand "crypto/rand" "crypto/sha256" "encoding/binary" "fmt" "math/bits" "math/rand" "sort" "strings" "sync" "time" ) type table struct { encode [256][][4]byte decode map[uint32]byte layout *byteLayout } type tableCacheKey struct { password string ascii string customTable string } var ( tableCache sync.Map tableSetCache sync.Map basePatternsOnce sync.Once basePatterns [][][4]byte basePatternsErr error ) type byteLayout struct { hintMask byte hintValue byte padMarker byte paddingPool []byte encodeHint func(group byte) byte encodeGroup func(group byte) byte decodeGroup func(b byte) (byte, bool) } func (l *byteLayout) isHint(b byte) bool { if (b & l.hintMask) == l.hintValue { return true } // ASCII layout maps 0x7f to '\n' to avoid DEL on the wire. return l.hintMask == 0x40 && b == '\n' } func getTable(config *Config) (*table, error) { tables, err := getTables(config) if err != nil { return nil, err } if len(tables) == 0 { return nil, fmt.Errorf("empty sudoku table set") } return tables[0], nil } func getTables(config *Config) ([]*table, error) { if config == nil { return nil, fmt.Errorf("nil sudoku config") } mode, err := normalizeASCII(config.GetAscii()) if err != nil { return nil, err } patterns, err := normalizedCustomPatterns(config, mode) if err != nil { return nil, err } cacheKey := tableCacheKey{ password: config.GetPassword(), ascii: mode, customTable: strings.Join(patterns, "\x00"), } if cached, ok := tableSetCache.Load(cacheKey); ok { return cached.([]*table), nil } tables := make([]*table, 0, len(patterns)) for _, pattern := range patterns { layout, err := resolveLayout(mode, pattern) if err != nil { return nil, err } t, err := buildTable(config.GetPassword(), layout) if err != nil { return nil, err } tables = append(tables, t) } actual, _ := tableSetCache.LoadOrStore(cacheKey, tables) return actual.([]*table), nil } func normalizedCustomPatterns(config *Config, mode string) ([]string, error) { if config == nil { return []string{""}, nil } if mode == "prefer_ascii" { return []string{""}, nil } rawPatterns := config.GetCustomTables() if len(rawPatterns) == 0 { rawPatterns = []string{config.GetCustomTable()} } patterns := make([]string, 0, len(rawPatterns)) seen := make(map[string]struct{}, len(rawPatterns)) for _, raw := range rawPatterns { pattern := strings.TrimSpace(raw) if pattern != "" { var err error pattern, err = normalizeCustomTable(pattern) if err != nil { return nil, err } } if _, ok := seen[pattern]; ok { continue } seen[pattern] = struct{}{} patterns = append(patterns, pattern) } if len(patterns) == 0 { return []string{""}, nil } return patterns, nil } func normalizedPadding(config *Config) (int, int) { if config == nil { return 0, 0 } pMin := int(config.GetPaddingMin()) pMax := int(config.GetPaddingMax()) if pMin > 100 { pMin = 100 } if pMax > 100 { pMax = 100 } if pMax < pMin { pMax = pMin } return pMin, pMax } func normalizeASCII(mode string) (string, error) { switch strings.ToLower(strings.TrimSpace(mode)) { case "", "entropy", "prefer_entropy": return "prefer_entropy", nil case "ascii", "prefer_ascii": return "prefer_ascii", nil default: return "", fmt.Errorf("invalid sudoku ascii mode: %s", mode) } } func normalizeCustomTable(pattern string) (string, error) { cleaned := strings.ToLower(strings.TrimSpace(pattern)) cleaned = strings.ReplaceAll(cleaned, " ", "") if len(cleaned) != 8 { return "", fmt.Errorf("customTable must be 8 chars, got %d", len(cleaned)) } var xCount, pCount, vCount int for _, ch := range cleaned { switch ch { case 'x': xCount++ case 'p': pCount++ case 'v': vCount++ default: return "", fmt.Errorf("customTable has invalid char %q", ch) } } if xCount != 2 || pCount != 2 || vCount != 4 { return "", fmt.Errorf("customTable must contain exactly 2 x, 2 p and 4 v") } return cleaned, nil } func resolveLayout(mode, customTable string) (*byteLayout, error) { if mode == "prefer_ascii" { return asciiLayout(), nil } if customTable != "" { return customLayout(customTable) } return entropyLayout(), nil } func asciiLayout() *byteLayout { padding := make([]byte, 0, 32) for i := 0; i < 32; i++ { padding = append(padding, byte(0x20+i)) } encodeGroup := func(group byte) byte { b := byte(0x40 | (group & 0x3f)) if b == 0x7f { return '\n' } return b } return &byteLayout{ hintMask: 0x40, hintValue: 0x40, padMarker: 0x3f, paddingPool: padding, encodeHint: encodeGroup, encodeGroup: encodeGroup, decodeGroup: func(b byte) (byte, bool) { if b == '\n' { return 0x3f, true } if (b & 0x40) == 0 { return 0, false } return b & 0x3f, true }, } } func entropyLayout() *byteLayout { padding := make([]byte, 0, 16) for i := 0; i < 8; i++ { padding = append(padding, byte(0x80+i), byte(0x10+i)) } encodeGroup := func(group byte) byte { v := group & 0x3f return ((v & 0x30) << 1) | (v & 0x0f) } return &byteLayout{ hintMask: 0x90, hintValue: 0x00, padMarker: 0x80, paddingPool: padding, encodeHint: encodeGroup, encodeGroup: encodeGroup, decodeGroup: func(b byte) (byte, bool) { if (b & 0x90) != 0 { return 0, false } return ((b >> 1) & 0x30) | (b & 0x0f), true }, } } func customLayout(pattern string) (*byteLayout, error) { pattern, err := normalizeCustomTable(pattern) if err != nil { return nil, err } var xBits, pBits, vBits []uint8 for i, c := range pattern { bit := uint8(7 - i) switch c { case 'x': xBits = append(xBits, bit) case 'p': pBits = append(pBits, bit) case 'v': vBits = append(vBits, bit) } } xMask := byte(0) for _, bit := range xBits { xMask |= 1 << bit } encodeGroupWithDropX := func(group byte, dropX int) byte { out := xMask if dropX >= 0 { out &^= 1 << xBits[dropX] } val := (group >> 4) & 0x03 pos := group & 0x0f if (val & 0x02) != 0 { out |= 1 << pBits[0] } if (val & 0x01) != 0 { out |= 1 << pBits[1] } for i, bit := range vBits { if (pos>>(3-uint8(i)))&0x01 == 1 { out |= 1 << bit } } return out } paddingSet := make(map[byte]struct{}, 64) padding := make([]byte, 0, 64) for drop := range xBits { for val := byte(0); val < 4; val++ { for pos := byte(0); pos < 16; pos++ { group := (val << 4) | pos b := encodeGroupWithDropX(group, drop) if bits.OnesCount8(b) >= 5 { if _, exists := paddingSet[b]; !exists { paddingSet[b] = struct{}{} padding = append(padding, b) } } } } } sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] }) if len(padding) == 0 { return nil, fmt.Errorf("customTable produced empty padding pool") } decodeGroup := func(b byte) (byte, bool) { if (b & xMask) != xMask { return 0, false } var val, pos byte if b&(1< in[1] { in[0], in[1] = in[1], in[0] } if in[2] > in[3] { in[2], in[3] = in[3], in[2] } if in[0] > in[2] { in[0], in[2] = in[2], in[0] } if in[1] > in[3] { in[1], in[3] = in[3], in[1] } if in[1] > in[2] { in[1], in[2] = in[2], in[1] } return in } func newSeededRand() *rand.Rand { seed := time.Now().UnixNano() var seedBytes [8]byte if _, err := crypto_rand.Read(seedBytes[:]); err == nil { seed = int64(binary.BigEndian.Uint64(seedBytes[:])) } return rand.New(rand.NewSource(seed)) }