From 96a108f8dab94b0f5e7af75e918d8c7d7517e083 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Sun, 9 Mar 2014 13:45:36 -0700 Subject: [PATCH] mark unicode normalization with type --- irc/channel.go | 54 +++++---- irc/client.go | 34 +++--- irc/client_lookup_set.go | 48 ++++---- irc/commands.go | 235 +++++++++++++++++++-------------------- irc/config.go | 6 +- irc/constants.go | 6 - irc/net.go | 16 +-- irc/reply.go | 54 ++++----- irc/server.go | 44 ++++---- irc/strings.go | 66 +++++++++++ irc/types.go | 20 ++-- irc/whowas.go | 10 +- 12 files changed, 324 insertions(+), 269 deletions(-) create mode 100644 irc/strings.go diff --git a/irc/channel.go b/irc/channel.go index 41f7f9a8..d985d6f7 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -3,27 +3,22 @@ package irc import ( "log" "strconv" - "strings" ) type Channel struct { flags ChannelModeSet lists map[ChannelMode]*UserMaskSet - key string + key Text members MemberSet - name string + name Name server *Server - topic string + topic Text userLimit uint64 } -func IsChannel(target string) bool { - return ChannelNameExpr.MatchString(target) -} - // NewChannel creates a new channel from a `Server` and a `name` // string, which must be unique on the server. -func NewChannel(s *Server, name string) *Channel { +func NewChannel(s *Server, name Name) *Channel { channel := &Channel{ flags: make(ChannelModeSet), lists: map[ChannelMode]*UserMaskSet{ @@ -32,7 +27,7 @@ func NewChannel(s *Server, name string) *Channel { InviteMask: NewUserMaskSet(), }, members: make(MemberSet), - name: strings.ToLower(name), + name: name, server: s, } @@ -73,22 +68,22 @@ func (channel *Channel) Nicks(target *Client) []string { nicks[i] += "+" } } - nicks[i] += client.Nick() + nicks[i] += client.Nick().String() i += 1 } return nicks } -func (channel *Channel) Id() string { +func (channel *Channel) Id() Name { return channel.name } -func (channel *Channel) Nick() string { +func (channel *Channel) Nick() Name { return channel.name } func (channel *Channel) String() string { - return channel.Id() + return channel.Id().String() } // @@ -117,7 +112,7 @@ func (channel *Channel) ModeString(client *Client) (str string) { // args for flags with args: The order must match above to keep // positional arguments in place. if showKey { - str += " " + channel.key + str += " " + channel.key.String() } if showUserLimit { str += " " + strconv.FormatUint(channel.userLimit, 10) @@ -131,11 +126,11 @@ func (channel *Channel) IsFull() bool { (uint64(len(channel.members)) >= channel.userLimit) } -func (channel *Channel) CheckKey(key string) bool { +func (channel *Channel) CheckKey(key Text) bool { return (channel.key == "") || (channel.key == key) } -func (channel *Channel) Join(client *Client, key string) { +func (channel *Channel) Join(client *Client, key Text) { if channel.members.Has(client) { // already joined, no message? return @@ -179,7 +174,7 @@ func (channel *Channel) Join(client *Client, key string) { channel.Names(client) } -func (channel *Channel) Part(client *Client, message string) { +func (channel *Channel) Part(client *Client, message Text) { if !channel.members.Has(client) { client.ErrNotOnChannel(channel) return @@ -207,7 +202,7 @@ func (channel *Channel) GetTopic(client *Client) { client.RplTopic(channel) } -func (channel *Channel) SetTopic(client *Client, topic string) { +func (channel *Channel) SetTopic(client *Client, topic Text) { if !(client.flags[Operator] || channel.members.Has(client)) { client.ErrNotOnChannel(channel) return @@ -244,7 +239,7 @@ func (channel *Channel) CanSpeak(client *Client) bool { return true } -func (channel *Channel) PrivMsg(client *Client, message string) { +func (channel *Channel) PrivMsg(client *Client, message Text) { if !channel.CanSpeak(client) { client.ErrCannotSendToChan(channel) return @@ -283,7 +278,7 @@ func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode, } func (channel *Channel) applyModeMember(client *Client, mode ChannelMode, - op ModeOp, nick string) bool { + op ModeOp, nick Name) bool { if !channel.ClientIsOperator(client) { client.ErrChanOPrivIsNeeded(channel) return false @@ -331,7 +326,7 @@ func (channel *Channel) ShowMaskList(client *Client, mode ChannelMode) { } func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, - mask string) bool { + mask Name) bool { list := channel.lists[mode] if list == nil { // This should never happen, but better safe than panicky. @@ -362,7 +357,8 @@ func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeO func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) bool { switch change.mode { case BanMask, ExceptMask, InviteMask: - return channel.applyModeMask(client, change.mode, change.op, change.arg) + return channel.applyModeMask(client, change.mode, change.op, + NewName(change.arg)) case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Persistent, Private: return channel.applyModeFlag(client, change.mode, change.op) @@ -379,11 +375,12 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo client.ErrNeedMoreParams("MODE") return false } - if change.arg == channel.key { + key := NewText(change.arg) + if key == channel.key { return false } - channel.key = change.arg + channel.key = key return true case Remove: @@ -405,7 +402,8 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo return true case ChannelOperator, Voice: - return channel.applyModeMember(client, change.mode, change.op, change.arg) + return channel.applyModeMember(client, change.mode, change.op, + NewName(change.arg)) default: client.ErrUnknownMode(change.mode, channel) @@ -456,7 +454,7 @@ func (channel *Channel) Persist() (err error) { return } -func (channel *Channel) Notice(client *Client, message string) { +func (channel *Channel) Notice(client *Client, message Text) { if !channel.CanSpeak(client) { client.ErrCannotSendToChan(channel) return @@ -478,7 +476,7 @@ func (channel *Channel) Quit(client *Client) { } } -func (channel *Channel) Kick(client *Client, target *Client, comment string) { +func (channel *Channel) Kick(client *Client, target *Client, comment Text) { if !(client.flags[Operator] || channel.members.Has(client)) { client.ErrNotOnChannel(channel) return diff --git a/irc/client.go b/irc/client.go index 2adb4309..a35f214d 100644 --- a/irc/client.go +++ b/irc/client.go @@ -7,14 +7,10 @@ import ( "time" ) -func IsNickname(nick string) bool { - return NicknameExpr.MatchString(nick) -} - type Client struct { atime time.Time authorized bool - awayMessage string + awayMessage Text capabilities CapabilitySet capState CapState channels ChannelSet @@ -23,16 +19,16 @@ type Client struct { flags map[UserMode]bool hasQuit bool hops uint - hostname string + hostname Name idleTimer *time.Timer loginTimer *time.Timer - nick string + nick Name phase Phase quitTimer *time.Timer - realname string + realname Text server *Server socket *Socket - username string + username Name } func NewClient(server *Server, conn net.Conn) *Client { @@ -186,27 +182,27 @@ func (c *Client) ModeString() (str string) { return } -func (c *Client) UserHost() string { +func (c *Client) UserHost() Name { username := "*" if c.HasUsername() { - username = c.username + username = c.username.String() } - return fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname) + return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname)) } -func (c *Client) Nick() string { +func (c *Client) Nick() Name { if c.HasNick() { return c.nick } - return "*" + return Name("*") } -func (c *Client) Id() string { +func (c *Client) Id() Name { return c.UserHost() } func (c *Client) String() string { - return c.Id() + return c.Id().String() } func (client *Client) Friends() ClientSet { @@ -220,12 +216,12 @@ func (client *Client) Friends() ClientSet { return friends } -func (client *Client) SetNickname(nickname string) { +func (client *Client) SetNickname(nickname Name) { client.nick = nickname client.server.clients.Add(client) } -func (client *Client) ChangeNickname(nickname string) { +func (client *Client) ChangeNickname(nickname Name) { // Make reply before changing nick to capture original source id. reply := RplNick(client, nickname) client.server.clients.Remove(client) @@ -244,7 +240,7 @@ func (client *Client) Reply(reply string, args ...interface{}) { client.socket.Write(reply) } -func (client *Client) Quit(message string) { +func (client *Client) Quit(message Text) { if client.hasQuit { return } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 261461d9..703f541e 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -25,36 +25,36 @@ func HasWildcards(mask string) bool { return wildMaskExpr.MatchString(mask) } -func ExpandUserHost(userhost string) (expanded string) { +func ExpandUserHost(userhost Name) (expanded Name) { expanded = userhost // fill in missing wildcards for nicks - if !strings.Contains(expanded, "!") { + if !strings.Contains(expanded.String(), "!") { expanded += "!*" } - if !strings.Contains(expanded, "@") { + if !strings.Contains(expanded.String(), "@") { expanded += "@*" } return } -func QuoteLike(userhost string) string { - return likeQuoter.Replace(userhost) +func QuoteLike(userhost Name) Name { + return Name(likeQuoter.Replace(userhost.String())) } type ClientLookupSet struct { - byNick map[string]*Client + byNick map[Name]*Client db *ClientDB } func NewClientLookupSet() *ClientLookupSet { return &ClientLookupSet{ - byNick: make(map[string]*Client), + byNick: make(map[Name]*Client), db: NewClientDB(), } } -func (clients *ClientLookupSet) Get(nick string) *Client { - return clients.byNick[strings.ToLower(nick)] +func (clients *ClientLookupSet) Get(nick Name) *Client { + return clients.byNick[nick.ToLower()] } func (clients *ClientLookupSet) Add(client *Client) error { @@ -64,7 +64,7 @@ func (clients *ClientLookupSet) Add(client *Client) error { if clients.Get(client.nick) != nil { return ErrNicknameInUse } - clients.byNick[strings.ToLower(client.nick)] = client + clients.byNick[client.Nick().ToLower()] = client clients.db.Add(client) return nil } @@ -76,12 +76,12 @@ func (clients *ClientLookupSet) Remove(client *Client) error { if clients.Get(client.nick) != client { return ErrNicknameMismatch } - delete(clients.byNick, strings.ToLower(client.nick)) + delete(clients.byNick, client.nick.ToLower()) clients.db.Remove(client) return nil } -func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { +func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) { userhost = ExpandUserHost(userhost) set = make(ClientSet) rows, err := clients.db.db.Query( @@ -94,7 +94,7 @@ func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { return } for rows.Next() { - var nickname string + var nickname Name err := rows.Scan(&nickname) if err != nil { if DEBUG_SERVER { @@ -114,12 +114,12 @@ func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { return } -func (clients *ClientLookupSet) Find(userhost string) *Client { +func (clients *ClientLookupSet) Find(userhost Name) *Client { userhost = ExpandUserHost(userhost) row := clients.db.db.QueryRow( `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\' LIMIT 1`, QuoteLike(userhost)) - var nickname string + var nickname Name err := row.Scan(&nickname) if err != nil { if DEBUG_SERVER { @@ -184,17 +184,17 @@ func (db *ClientDB) Remove(client *Client) { // type UserMaskSet struct { - masks map[string]bool + masks map[Name]bool regexp *regexp.Regexp } func NewUserMaskSet() *UserMaskSet { return &UserMaskSet{ - masks: make(map[string]bool), + masks: make(map[Name]bool), } } -func (set *UserMaskSet) Add(mask string) bool { +func (set *UserMaskSet) Add(mask Name) bool { if set.masks[mask] { return false } @@ -203,7 +203,7 @@ func (set *UserMaskSet) Add(mask string) bool { return true } -func (set *UserMaskSet) AddAll(masks []string) (added bool) { +func (set *UserMaskSet) AddAll(masks []Name) (added bool) { for _, mask := range masks { if !added && !set.masks[mask] { added = true @@ -214,7 +214,7 @@ func (set *UserMaskSet) AddAll(masks []string) (added bool) { return } -func (set *UserMaskSet) Remove(mask string) bool { +func (set *UserMaskSet) Remove(mask Name) bool { if !set.masks[mask] { return false } @@ -223,18 +223,18 @@ func (set *UserMaskSet) Remove(mask string) bool { return true } -func (set *UserMaskSet) Match(userhost string) bool { +func (set *UserMaskSet) Match(userhost Name) bool { if set.regexp == nil { return false } - return set.regexp.MatchString(userhost) + return set.regexp.MatchString(userhost.String()) } func (set *UserMaskSet) String() string { masks := make([]string, len(set.masks)) index := 0 for mask := range set.masks { - masks[index] = mask + masks[index] = mask.String() index += 1 } return strings.Join(masks, " ") @@ -255,7 +255,7 @@ func (set *UserMaskSet) setRegexp() { maskExprs := make([]string, len(set.masks)) index := 0 for mask := range set.masks { - manyParts := strings.Split(mask, "*") + manyParts := strings.Split(mask.String(), "*") manyExprs := make([]string, len(manyParts)) for mindex, manyPart := range manyParts { oneParts := strings.Split(manyPart, "?") diff --git a/irc/commands.go b/irc/commands.go index 976d8987..dca13a7b 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -1,7 +1,6 @@ package irc import ( - "code.google.com/p/go.text/unicode/norm" "errors" "fmt" "regexp" @@ -114,14 +113,14 @@ func ParseLine(line string) (command StringCode, args []string) { _, line = splitArg(line) } arg, line := splitArg(line) - command = StringCode(strings.ToUpper(arg)) + command = StringCode(NewName(strings.ToUpper(arg))) for len(line) > 0 { if strings.HasPrefix(line, ":") { - args = append(args, norm.NFC.String(line[len(":"):])) + args = append(args, line[len(":"):]) break } arg, line = splitArg(line) - args = append(args, norm.NFKC.String(arg)) + args = append(args, arg) } return } @@ -147,8 +146,8 @@ func NewUnknownCommand(args []string) *UnknownCommand { type PingCommand struct { BaseCommand - server string - server2 string + server Name + server2 Name } func (cmd *PingCommand) String() string { @@ -160,10 +159,10 @@ func NewPingCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } msg := &PingCommand{ - server: args[0], + server: NewName(args[0]), } if len(args) > 1 { - msg.server2 = args[1] + msg.server2 = NewName(args[1]) } return msg, nil } @@ -172,8 +171,8 @@ func NewPingCommand(args []string) (editableCommand, error) { type PongCommand struct { BaseCommand - server1 string - server2 string + server1 Name + server2 Name } func (cmd *PongCommand) String() string { @@ -185,10 +184,10 @@ func NewPongCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } message := &PongCommand{ - server1: args[0], + server1: NewName(args[0]), } if len(args) > 1 { - message.server2 = args[1] + message.server2 = NewName(args[1]) } return message, nil } @@ -230,7 +229,7 @@ func NewPassCommand(args []string) (editableCommand, error) { type NickCommand struct { BaseCommand - nickname string + nickname Name } func (m *NickCommand) String() string { @@ -242,21 +241,21 @@ func NewNickCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } return &NickCommand{ - nickname: args[0], + nickname: NewName(args[0]), }, nil } type UserCommand struct { BaseCommand - username string - realname string + username Name + realname Text } // USER type RFC1459UserCommand struct { UserCommand - hostname string - servername string + hostname Name + servername Name } func (cmd *RFC1459UserCommand) String() string { @@ -297,17 +296,17 @@ func NewUserCommand(args []string) (editableCommand, error) { mode: uint8(mode), unused: args[2], } - msg.username = args[0] - msg.realname = args[3] + msg.username = NewName(args[0]) + msg.realname = NewText(args[3]) return msg, nil } msg := &RFC1459UserCommand{ - hostname: args[1], - servername: args[2], + hostname: NewName(args[1]), + servername: NewName(args[2]), } - msg.username = args[0] - msg.realname = args[3] + msg.username = NewName(args[0]) + msg.realname = NewText(args[3]) return msg, nil } @@ -315,7 +314,7 @@ func NewUserCommand(args []string) (editableCommand, error) { type QuitCommand struct { BaseCommand - message string + message Text } func (cmd *QuitCommand) String() string { @@ -325,7 +324,7 @@ func (cmd *QuitCommand) String() string { func NewQuitCommand(args []string) (editableCommand, error) { msg := &QuitCommand{} if len(args) > 0 { - msg.message = args[0] + msg.message = NewText(args[0]) } return msg, nil } @@ -334,7 +333,7 @@ func NewQuitCommand(args []string) (editableCommand, error) { type JoinCommand struct { BaseCommand - channels map[string]string + channels map[Name]Text zero bool } @@ -344,7 +343,7 @@ func (cmd *JoinCommand) String() string { func NewJoinCommand(args []string) (editableCommand, error) { msg := &JoinCommand{ - channels: make(map[string]string), + channels: make(map[Name]Text), } if len(args) == 0 { @@ -364,7 +363,7 @@ func NewJoinCommand(args []string) (editableCommand, error) { } } for i, channel := range channels { - msg.channels[channel] = keys[i] + msg.channels[NewName(channel)] = NewText(keys[i]) } return msg, nil @@ -374,13 +373,13 @@ func NewJoinCommand(args []string) (editableCommand, error) { type PartCommand struct { BaseCommand - channels []string - message string + channels []Name + message Text } -func (cmd *PartCommand) Message() string { +func (cmd *PartCommand) Message() Text { if cmd.message == "" { - return cmd.Client().Nick() + return cmd.Client().Nick().Text() } return cmd.message } @@ -394,10 +393,10 @@ func NewPartCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } msg := &PartCommand{ - channels: strings.Split(args[0], ","), + channels: NewNames(strings.Split(args[0], ",")), } if len(args) > 1 { - msg.message = args[1] + msg.message = NewText(args[1]) } return msg, nil } @@ -406,8 +405,8 @@ func NewPartCommand(args []string) (editableCommand, error) { type PrivMsgCommand struct { BaseCommand - target string - message string + target Name + message Text } func (cmd *PrivMsgCommand) String() string { @@ -419,8 +418,8 @@ func NewPrivMsgCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } return &PrivMsgCommand{ - target: args[0], - message: args[1], + target: NewName(args[0]), + message: NewText(args[1]), }, nil } @@ -428,9 +427,9 @@ func NewPrivMsgCommand(args []string) (editableCommand, error) { type TopicCommand struct { BaseCommand - channel string + channel Name setTopic bool - topic string + topic Text } func (cmd *TopicCommand) String() string { @@ -442,11 +441,11 @@ func NewTopicCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } msg := &TopicCommand{ - channel: args[0], + channel: NewName(args[0]), } if len(args) > 1 { msg.setTopic = true - msg.topic = args[1] + msg.topic = NewText(args[1]) } return msg, nil } @@ -482,18 +481,18 @@ func (changes ModeChanges) String() string { type ModeCommand struct { BaseCommand - nickname string + nickname Name changes ModeChanges } // MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) -func NewUserModeCommand(args []string) (editableCommand, error) { +func NewUserModeCommand(nickname Name, args []string) (editableCommand, error) { cmd := &ModeCommand{ - nickname: args[0], + nickname: nickname, changes: make(ModeChanges, 0), } - for _, modeChange := range args[1:] { + for _, modeChange := range args { if len(modeChange) == 0 { continue } @@ -559,17 +558,16 @@ func (changes ChannelModeChanges) String() (str string) { type ChannelModeCommand struct { BaseCommand - channel string + channel Name changes ChannelModeChanges } // MODE *( ( "-" / "+" ) * * ) -func NewChannelModeCommand(args []string) (editableCommand, error) { +func NewChannelModeCommand(channel Name, args []string) (editableCommand, error) { cmd := &ChannelModeCommand{ - channel: args[0], + channel: channel, changes: make(ChannelModeChanges, 0), } - args = args[1:] for len(args) > 0 { if len(args[0]) == 0 { @@ -616,17 +614,18 @@ func NewModeCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } - if IsChannel(args[0]) { - return NewChannelModeCommand(args) + name := NewName(args[0]) + if name.IsChannel() { + return NewChannelModeCommand(name, args[1:]) } else { - return NewUserModeCommand(args) + return NewUserModeCommand(name, args[1:]) } } type WhoisCommand struct { BaseCommand - target string - masks []string + target Name + masks []Name } // WHOIS [ ] *( "," ) @@ -646,8 +645,8 @@ func NewWhoisCommand(args []string) (editableCommand, error) { } return &WhoisCommand{ - target: target, - masks: strings.Split(masks, ","), + target: NewName(target), + masks: NewNames(strings.Split(masks, ",")), }, nil } @@ -657,7 +656,7 @@ func (msg *WhoisCommand) String() string { type WhoCommand struct { BaseCommand - mask string + mask Name operatorOnly bool } @@ -666,7 +665,7 @@ func NewWhoCommand(args []string) (editableCommand, error) { cmd := &WhoCommand{} if len(args) > 0 { - cmd.mask = args[0] + cmd.mask = NewName(args[0]) } if (len(args) > 1) && (args[1] == "o") { @@ -682,7 +681,7 @@ func (msg *WhoCommand) String() string { type OperCommand struct { PassCommand - name string + name Name } func (msg *OperCommand) String() string { @@ -700,7 +699,7 @@ func NewOperCommand(args []string) (editableCommand, error) { } cmd := &OperCommand{ - name: args[0], + name: NewName(args[0]), } cmd.password = []byte(args[1]) return cmd, nil @@ -739,12 +738,12 @@ func NewCapCommand(args []string) (editableCommand, error) { // HAPROXY support type ProxyCommand struct { BaseCommand - net string - sourceIP string - destIP string - sourcePort string - destPort string - hostname string // looked up in socket thread + net Name + sourceIP Name + destIP Name + sourcePort Name + destPort Name + hostname Name // looked up in socket thread } func (msg *ProxyCommand) String() string { @@ -756,18 +755,18 @@ func NewProxyCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } return &ProxyCommand{ - net: args[0], - sourceIP: args[1], - destIP: args[2], - sourcePort: args[3], - destPort: args[4], - hostname: LookupHostname(args[1]), + net: NewName(args[0]), + sourceIP: NewName(args[1]), + destIP: NewName(args[2]), + sourcePort: NewName(args[3]), + destPort: NewName(args[4]), + hostname: LookupHostname(NewName(args[1])), }, nil } type AwayCommand struct { BaseCommand - text string + text Text away bool } @@ -779,7 +778,7 @@ func NewAwayCommand(args []string) (editableCommand, error) { cmd := &AwayCommand{} if len(args) > 0 { - cmd.text = args[0] + cmd.text = NewText(args[0]) cmd.away = true } @@ -788,7 +787,7 @@ func NewAwayCommand(args []string) (editableCommand, error) { type IsOnCommand struct { BaseCommand - nicks []string + nicks []Name } func (msg *IsOnCommand) String() string { @@ -801,27 +800,27 @@ func NewIsOnCommand(args []string) (editableCommand, error) { } return &IsOnCommand{ - nicks: args, + nicks: NewNames(args), }, nil } type MOTDCommand struct { BaseCommand - target string + target Name } func NewMOTDCommand(args []string) (editableCommand, error) { cmd := &MOTDCommand{} if len(args) > 0 { - cmd.target = args[0] + cmd.target = NewName(args[0]) } return cmd, nil } type NoticeCommand struct { BaseCommand - target string - message string + target Name + message Text } func (cmd *NoticeCommand) String() string { @@ -833,20 +832,20 @@ func NewNoticeCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } return &NoticeCommand{ - target: args[0], - message: args[1], + target: NewName(args[0]), + message: NewText(args[1]), }, nil } type KickCommand struct { BaseCommand - kicks map[string]string - comment string + kicks map[Name]Name + comment Text } -func (msg *KickCommand) Comment() string { +func (msg *KickCommand) Comment() Text { if msg.comment == "" { - return msg.Client().Nick() + return msg.Client().Nick().Text() } return msg.comment } @@ -855,13 +854,13 @@ func NewKickCommand(args []string) (editableCommand, error) { if len(args) < 2 { return nil, NotEnoughArgsError } - channels := strings.Split(args[0], ",") - users := strings.Split(args[1], ",") + channels := NewNames(strings.Split(args[0], ",")) + users := NewNames(strings.Split(args[1], ",")) if (len(channels) != len(users)) && (len(users) != 1) { return nil, NotEnoughArgsError } cmd := &KickCommand{ - kicks: make(map[string]string), + kicks: make(map[Name]Name), } for index, channel := range channels { if len(users) == 1 { @@ -871,48 +870,48 @@ func NewKickCommand(args []string) (editableCommand, error) { } } if len(args) > 2 { - cmd.comment = args[2] + cmd.comment = NewText(args[2]) } return cmd, nil } type ListCommand struct { BaseCommand - channels []string - target string + channels []Name + target Name } func NewListCommand(args []string) (editableCommand, error) { cmd := &ListCommand{} if len(args) > 0 { - cmd.channels = strings.Split(args[0], ",") + cmd.channels = NewNames(strings.Split(args[0], ",")) } if len(args) > 1 { - cmd.target = args[1] + cmd.target = NewName(args[1]) } return cmd, nil } type NamesCommand struct { BaseCommand - channels []string - target string + channels []Name + target Name } func NewNamesCommand(args []string) (editableCommand, error) { cmd := &NamesCommand{} if len(args) > 0 { - cmd.channels = strings.Split(args[0], ",") + cmd.channels = NewNames(strings.Split(args[0], ",")) } if len(args) > 1 { - cmd.target = args[1] + cmd.target = NewName(args[1]) } return cmd, nil } type DebugCommand struct { BaseCommand - subCommand string + subCommand Name } func NewDebugCommand(args []string) (editableCommand, error) { @@ -921,27 +920,27 @@ func NewDebugCommand(args []string) (editableCommand, error) { } return &DebugCommand{ - subCommand: strings.ToUpper(args[0]), + subCommand: NewName(strings.ToUpper(args[0])), }, nil } type VersionCommand struct { BaseCommand - target string + target Name } func NewVersionCommand(args []string) (editableCommand, error) { cmd := &VersionCommand{} if len(args) > 0 { - cmd.target = args[0] + cmd.target = NewName(args[0]) } return cmd, nil } type InviteCommand struct { BaseCommand - nickname string - channel string + nickname Name + channel Name } func NewInviteCommand(args []string) (editableCommand, error) { @@ -950,28 +949,28 @@ func NewInviteCommand(args []string) (editableCommand, error) { } return &InviteCommand{ - nickname: args[0], - channel: args[1], + nickname: NewName(args[0]), + channel: NewName(args[1]), }, nil } type TimeCommand struct { BaseCommand - target string + target Name } func NewTimeCommand(args []string) (editableCommand, error) { cmd := &TimeCommand{} if len(args) > 0 { - cmd.target = args[0] + cmd.target = NewName(args[0]) } return cmd, nil } type KillCommand struct { BaseCommand - nickname string - comment string + nickname Name + comment Text } func NewKillCommand(args []string) (editableCommand, error) { @@ -979,16 +978,16 @@ func NewKillCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } return &KillCommand{ - nickname: args[0], - comment: args[1], + nickname: NewName(args[0]), + comment: NewText(args[1]), }, nil } type WhoWasCommand struct { BaseCommand - nicknames []string + nicknames []Name count int64 - target string + target Name } func NewWhoWasCommand(args []string) (editableCommand, error) { @@ -996,13 +995,13 @@ func NewWhoWasCommand(args []string) (editableCommand, error) { return nil, NotEnoughArgsError } cmd := &WhoWasCommand{ - nicknames: strings.Split(args[0], ","), + nicknames: NewNames(strings.Split(args[0], ",")), } if len(args) > 1 { cmd.count, _ = strconv.ParseInt(args[1], 10, 64) } if len(args) > 2 { - cmd.target = args[2] + cmd.target = NewName(args[2]) } return cmd, nil } diff --git a/irc/config.go b/irc/config.go index 6c5aa14d..480394d0 100644 --- a/irc/config.go +++ b/irc/config.go @@ -37,10 +37,10 @@ type Config struct { } } -func (conf *Config) Operators() map[string][]byte { - operators := make(map[string][]byte) +func (conf *Config) Operators() map[Name][]byte { + operators := make(map[Name][]byte) for name, opConf := range conf.Operator { - operators[name] = opConf.PasswordBytes() + operators[NewName(name)] = opConf.PasswordBytes() } return operators } diff --git a/irc/constants.go b/irc/constants.go index d2709468..9a3252b9 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -2,7 +2,6 @@ package irc import ( "errors" - "regexp" "time" ) @@ -15,11 +14,6 @@ var ( // errors ErrAlreadyDestroyed = errors.New("already destroyed") - - // regexps - ChannelNameExpr = regexp.MustCompile(`^[&!#+][\pL\pN]{1,63}$`) - NicknameExpr = regexp.MustCompile( - "^[\\pL\\pN\\pP\\pS]{1,32}$") ) const ( diff --git a/irc/net.go b/irc/net.go index 1653112b..d2dba3a8 100644 --- a/irc/net.go +++ b/irc/net.go @@ -5,25 +5,25 @@ import ( "strings" ) -func IPString(addr net.Addr) string { +func IPString(addr net.Addr) Name { addrStr := addr.String() ipaddr, _, err := net.SplitHostPort(addrStr) if err != nil { - return addrStr + return Name(addrStr) } - return ipaddr + return Name(ipaddr) } -func AddrLookupHostname(addr net.Addr) string { +func AddrLookupHostname(addr net.Addr) Name { return LookupHostname(IPString(addr)) } -func LookupHostname(addr string) string { - names, err := net.LookupAddr(addr) +func LookupHostname(addr Name) Name { + names, err := net.LookupAddr(addr.String()) if err != nil { - return addr + return Name(addr) } hostname := strings.TrimSuffix(names[0], ".") - return hostname + return Name(hostname) } diff --git a/irc/reply.go b/irc/reply.go index 72002f93..dfd66915 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -79,23 +79,23 @@ func (target *Client) MultilineReply(names []string, code NumericCode, format st // messaging replies // -func RplPrivMsg(source Identifier, target Identifier, message string) string { +func RplPrivMsg(source Identifier, target Identifier, message Text) string { return NewStringReply(source, PRIVMSG, "%s :%s", target.Nick(), message) } -func RplNotice(source Identifier, target Identifier, message string) string { +func RplNotice(source Identifier, target Identifier, message Text) string { return NewStringReply(source, NOTICE, "%s :%s", target.Nick(), message) } -func RplNick(source Identifier, newNick string) string { - return NewStringReply(source, NICK, newNick) +func RplNick(source Identifier, newNick Name) string { + return NewStringReply(source, NICK, newNick.String()) } func RplJoin(client *Client, channel *Channel) string { - return NewStringReply(client, JOIN, channel.name) + return NewStringReply(client, JOIN, channel.name.String()) } -func RplPart(client *Client, channel *Channel, message string) string { +func RplPart(client *Client, channel *Channel, message Text) string { return NewStringReply(client, PART, "%s :%s", channel, message) } @@ -117,10 +117,10 @@ func RplPing(target Identifier) string { } func RplPong(client *Client) string { - return NewStringReply(nil, PONG, client.Nick()) + return NewStringReply(nil, PONG, client.Nick().String()) } -func RplQuit(client *Client, message string) string { +func RplQuit(client *Client, message Text) string { return NewStringReply(client, QUIT, ":%s", message) } @@ -128,16 +128,16 @@ func RplError(message string) string { return NewStringReply(nil, ERROR, ":%s", message) } -func RplInviteMsg(inviter *Client, invitee *Client, channel string) string { +func RplInviteMsg(inviter *Client, invitee *Client, channel Name) string { return NewStringReply(inviter, INVITE, "%s :%s", invitee.Nick(), channel) } -func RplKick(channel *Channel, client *Client, target *Client, comment string) string { +func RplKick(channel *Channel, client *Client, target *Client, comment Text) string { return NewStringReply(client, KICK, "%s %s :%s", channel, target.Nick(), comment) } -func RplKill(client *Client, target *Client, comment string) string { +func RplKill(client *Client, target *Client, comment Text) string { return NewStringReply(client, KICK, "%s :%s", target.Nick(), comment) } @@ -184,7 +184,7 @@ func (target *Client) RplTopic(channel *Channel) { // // NB: correction in errata -func (target *Client) RplInvitingMsg(invitee *Client, channel string) { +func (target *Client) RplInvitingMsg(invitee *Client, channel Name) { target.NumericReply(RPL_INVITING, "%s %s", invitee.Nick(), channel) } @@ -253,7 +253,7 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) { } if channel != nil { - channelName = channel.name + channelName = channel.name.String() if target.capabilities[MultiPrefix] { if channel.members[client][ChannelOperator] { flags += "@" @@ -275,12 +275,12 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) { } // :End of WHO list -func (target *Client) RplEndOfWho(name string) { +func (target *Client) RplEndOfWho(name Name) { target.NumericReply(RPL_ENDOFWHO, "%s :End of WHO list", name) } -func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask string) { +func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask Name) { switch mode { case BanMask: target.RplBanList(channel, mask) @@ -306,7 +306,7 @@ func (target *Client) RplEndOfMaskList(mode ChannelMode, channel *Channel) { } } -func (target *Client) RplBanList(channel *Channel, mask string) { +func (target *Client) RplBanList(channel *Channel, mask Name) { target.NumericReply(RPL_BANLIST, "%s %s", channel, mask) } @@ -316,7 +316,7 @@ func (target *Client) RplEndOfBanList(channel *Channel) { "%s :End of channel ban list", channel) } -func (target *Client) RplExceptList(channel *Channel, mask string) { +func (target *Client) RplExceptList(channel *Channel, mask Name) { target.NumericReply(RPL_EXCEPTLIST, "%s %s", channel, mask) } @@ -326,7 +326,7 @@ func (target *Client) RplEndOfExceptList(channel *Channel) { "%s :End of channel exception list", channel) } -func (target *Client) RplInviteList(channel *Channel, mask string) { +func (target *Client) RplInviteList(channel *Channel, mask Name) { target.NumericReply(RPL_INVITELIST, "%s %s", channel, mask) } @@ -396,7 +396,7 @@ func (target *Client) RplVersion() { "%s %s", SEM_VER, target.server.name) } -func (target *Client) RplInviting(invitee *Client, channel string) { +func (target *Client) RplInviting(invitee *Client, channel Name) { target.NumericReply(RPL_INVITING, "%s %s", invitee.Nick(), channel) } @@ -412,7 +412,7 @@ func (target *Client) RplWhoWasUser(whoWas *WhoWas) { whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname) } -func (target *Client) RplEndOfWhoWas(nickname string) { +func (target *Client) RplEndOfWhoWas(nickname Name) { target.NumericReply(RPL_ENDOFWHOWAS, "%s :End of WHOWAS", nickname) } @@ -426,7 +426,7 @@ func (target *Client) ErrAlreadyRegistered() { ":You may not reregister") } -func (target *Client) ErrNickNameInUse(nick string) { +func (target *Client) ErrNickNameInUse(nick Name) { target.NumericReply(ERR_NICKNAMEINUSE, "%s :Nickname is already in use", nick) } @@ -441,12 +441,12 @@ func (target *Client) ErrUsersDontMatch() { ":Cannot change mode for other users") } -func (target *Client) ErrNeedMoreParams(command string) { +func (target *Client) ErrNeedMoreParams(command StringCode) { target.NumericReply(ERR_NEEDMOREPARAMS, "%s :Not enough parameters", command) } -func (target *Client) ErrNoSuchChannel(channel string) { +func (target *Client) ErrNoSuchChannel(channel Name) { target.NumericReply(ERR_NOSUCHCHANNEL, "%s :No such channel", channel) } @@ -471,7 +471,7 @@ func (target *Client) ErrBadChannelKey(channel *Channel) { "%s :Cannot join channel (+k)", channel.name) } -func (target *Client) ErrNoSuchNick(nick string) { +func (target *Client) ErrNoSuchNick(nick Name) { target.NumericReply(ERR_NOSUCHNICK, "%s :No such nick/channel", nick) } @@ -493,7 +493,7 @@ func (target *Client) ErrRestricted() { target.NumericReply(ERR_RESTRICTED, ":Your connection is restricted!") } -func (target *Client) ErrNoSuchServer(server string) { +func (target *Client) ErrNoSuchServer(server Name) { target.NumericReply(ERR_NOSUCHSERVER, "%s :No such server", server) } @@ -521,7 +521,7 @@ func (target *Client) ErrNoNicknameGiven() { target.NumericReply(ERR_NONICKNAMEGIVEN, ":No nickname given") } -func (target *Client) ErrErroneusNickname(nick string) { +func (target *Client) ErrErroneusNickname(nick Name) { target.NumericReply(ERR_ERRONEUSNICKNAME, "%s :Erroneous nickname", nick) } @@ -536,7 +536,7 @@ func (target *Client) ErrChannelIsFull(channel *Channel) { "%s :Cannot join channel (+l)", channel) } -func (target *Client) ErrWasNoSuchNick(nickname string) { +func (target *Client) ErrWasNoSuchNick(nickname Name) { target.NumericReply(ERR_WASNOSUCHNICK, "%s :There was no such nickname", nickname) } diff --git a/irc/server.go b/irc/server.go index 9c7acae9..dc2256ff 100644 --- a/irc/server.go +++ b/irc/server.go @@ -24,9 +24,9 @@ type Server struct { db *sql.DB idle chan *Client motdFile string - name string + name Name newConns chan net.Conn - operators map[string][]byte + operators map[Name][]byte password []byte signals chan os.Signal whoWas *WhoWasList @@ -41,7 +41,7 @@ func NewServer(config *Config) *Server { db: OpenDB(config.Server.Database), idle: make(chan *Client, 16), motdFile: config.Server.MOTD, - name: config.Server.Name, + name: NewName(config.Server.Name), newConns: make(chan net.Conn, 16), operators: config.Operators(), signals: make(chan os.Signal, 1), @@ -68,7 +68,7 @@ func loadChannelList(channel *Channel, list string, maskMode ChannelMode) { if list == "" { return } - channel.lists[maskMode].AddAll(strings.Split(list, " ")) + channel.lists[maskMode].AddAll(NewNames(strings.Split(list, " "))) } func (server *Server) loadChannels() { @@ -80,7 +80,9 @@ func (server *Server) loadChannels() { log.Fatal("error loading channels: ", err) } for rows.Next() { - var name, flags, key, topic string + var name Name + var flags string + var key, topic Text var userLimit uint64 var banList, exceptList, inviteList string err = rows.Scan(&name, &flags, &key, &topic, &userLimit, &banList, @@ -248,15 +250,15 @@ func (server *Server) MOTD(client *Client) { client.RplMOTDEnd() } -func (s *Server) Id() string { +func (s *Server) Id() Name { return s.name } func (s *Server) String() string { - return s.name + return s.name.String() } -func (s *Server) Nick() string { +func (s *Server) Nick() Name { return s.Id() } @@ -339,7 +341,7 @@ func (m *NickCommand) HandleRegServer(s *Server) { return } - if !IsNickname(m.nickname) { + if !m.nickname.IsNickname() { client.ErrErroneusNickname(m.nickname) return } @@ -416,7 +418,7 @@ func (msg *NickCommand) HandleServer(server *Server) { return } - if !IsNickname(msg.nickname) { + if !msg.nickname.IsNickname() { client.ErrErroneusNickname(msg.nickname) return } @@ -447,13 +449,13 @@ func (m *JoinCommand) HandleServer(s *Server) { if m.zero { for channel := range client.channels { - channel.Part(client, client.Nick()) + channel.Part(client, client.Nick().Text()) } return } for name, key := range m.channels { - if !IsChannel(name) { + if !name.IsChannel() { client.ErrNoSuchChannel(name) continue } @@ -497,7 +499,7 @@ func (msg *TopicCommand) HandleServer(server *Server) { func (msg *PrivMsgCommand) HandleServer(server *Server) { client := msg.Client() - if IsChannel(msg.target) { + if msg.target.IsChannel() { channel := server.channels.Get(msg.target) if channel == nil { client.ErrNoSuchChannel(msg.target) @@ -577,13 +579,13 @@ func (client *Client) WhoisChannelsNames() []string { for channel := range client.channels { switch { case channel.members[client][ChannelOperator]: - chstrs[index] = "@" + channel.name + chstrs[index] = "@" + channel.name.String() case channel.members[client][Voice]: - chstrs[index] = "+" + channel.name + chstrs[index] = "+" + channel.name.String() default: - chstrs[index] = channel.name + chstrs[index] = channel.name.String() } index += 1 } @@ -635,7 +637,7 @@ func (msg *WhoCommand) HandleServer(server *Server) { for _, channel := range server.channels { whoChannel(client, channel, friends) } - } else if IsChannel(mask) { + } else if mask.IsChannel() { // TODO implement wildcard matching channel := server.channels.Get(mask) if channel != nil { @@ -685,7 +687,7 @@ func (msg *IsOnCommand) HandleServer(server *Server) { ison := make([]string, 0) for _, nick := range msg.nicks { if iclient := server.clients.Get(nick); iclient != nil { - ison = append(ison, iclient.Nick()) + ison = append(ison, iclient.Nick().String()) } } @@ -698,7 +700,7 @@ func (msg *MOTDCommand) HandleServer(server *Server) { func (msg *NoticeCommand) HandleServer(server *Server) { client := msg.Client() - if IsChannel(msg.target) { + if msg.target.IsChannel() { channel := server.channels.Get(msg.target) if channel == nil { client.ErrNoSuchChannel(msg.target) @@ -785,7 +787,7 @@ func (msg *NamesCommand) HandleServer(server *Server) { } func (server *Server) Reply(target *Client, format string, args ...interface{}) { - target.Reply(RplPrivMsg(server, target, fmt.Sprintf(format, args...))) + target.Reply(RplPrivMsg(server, target, NewText(fmt.Sprintf(format, args...)))) } func (msg *DebugCommand) HandleServer(server *Server) { @@ -880,7 +882,7 @@ func (msg *KillCommand) HandleServer(server *Server) { } quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), msg.comment) - target.Quit(quitMsg) + target.Quit(NewText(quitMsg)) } func (msg *WhoWasCommand) HandleServer(server *Server) { diff --git a/irc/strings.go b/irc/strings.go new file mode 100644 index 00000000..dfd01fcb --- /dev/null +++ b/irc/strings.go @@ -0,0 +1,66 @@ +package irc + +import ( + "code.google.com/p/go.text/unicode/norm" + "regexp" + "strings" +) + +var ( + // regexps + ChannelNameExpr = regexp.MustCompile(`^[&!#+][\pL\pN]{1,63}$`) + NicknameExpr = regexp.MustCompile("^[\\pL\\pN\\pP\\pS]{1,32}$") +) + +// Names are normalized and canonicalized to remove formatting marks +// and simplify usage. They are things like hostnames and usermasks. +type Name string + +func NewName(str string) Name { + return Name(norm.NFKC.String(str)) +} + +func NewNames(strs []string) []Name { + names := make([]Name, len(strs)) + for index, str := range strs { + names[index] = NewName(str) + } + return names +} + +// tests + +func (name Name) IsChannel() bool { + return ChannelNameExpr.MatchString(name.String()) +} + +func (name Name) IsNickname() bool { + return NicknameExpr.MatchString(name.String()) +} + +// conversions + +func (name Name) String() string { + return string(name) +} + +func (name Name) ToLower() Name { + return Name(strings.ToLower(name.String())) +} + +// It's safe to coerce a Name to Text. Name is a strict subset of Text. +func (name Name) Text() Text { + return Text(name) +} + +// Text is PRIVMSG, NOTICE, or TOPIC data. It's canonicalized UTF8 +// data to simplify but keeps all formatting. +type Text string + +func NewText(str string) Text { + return Text(norm.NFC.String(str)) +} + +func (text Text) String() string { + return string(text) +} diff --git a/irc/types.go b/irc/types.go index 19014012..ecf53d42 100644 --- a/irc/types.go +++ b/irc/types.go @@ -67,7 +67,7 @@ type ReplyCode interface { String() string } -type StringCode string +type StringCode Name func (code StringCode) String() string { return string(code) @@ -86,25 +86,25 @@ func (mode ChannelMode) String() string { return string(mode) } -type ChannelNameMap map[string]*Channel +type ChannelNameMap map[Name]*Channel -func (channels ChannelNameMap) Get(name string) *Channel { - return channels[strings.ToLower(name)] +func (channels ChannelNameMap) Get(name Name) *Channel { + return channels[name.ToLower()] } func (channels ChannelNameMap) Add(channel *Channel) error { - if channels[channel.name] != nil { + if channels[channel.name.ToLower()] != nil { return fmt.Errorf("%s: already set", channel.name) } - channels[channel.name] = channel + channels[channel.name.ToLower()] = channel return nil } func (channels ChannelNameMap) Remove(channel *Channel) error { - if channel != channels[channel.name] { + if channel != channels[channel.name.ToLower()] { return fmt.Errorf("%s: mismatch", channel.name) } - delete(channels, channel.name) + delete(channels, channel.name.ToLower()) return nil } @@ -182,8 +182,8 @@ func (channels ChannelSet) First() *Channel { // type Identifier interface { - Id() string - Nick() string + Id() Name + Nick() Name } type Replier interface { diff --git a/irc/whowas.go b/irc/whowas.go index 008ed7f3..e1a4686c 100644 --- a/irc/whowas.go +++ b/irc/whowas.go @@ -7,10 +7,10 @@ type WhoWasList struct { } type WhoWas struct { - nickname string - username string - hostname string - realname string + nickname Name + username Name + hostname Name + realname Text } func NewWhoWasList(size uint) *WhoWasList { @@ -32,7 +32,7 @@ func (list *WhoWasList) Append(client *Client) { } } -func (list *WhoWasList) Find(nickname string, limit int64) []*WhoWas { +func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas { results := make([]*WhoWas, 0) for whoWas := range list.Each() { if nickname != whoWas.nickname {