Changeset 478 in code for trunk/irc.go


Ignore:
Timestamp:
Mar 24, 2021, 5:15:52 PM (4 years ago)
Author:
hubert
Message:

Implement casemapping

TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)

What is casemapping?

see <https://modern.ircdocs.horse/#casemapping-parameter>

Casemapping and multi-upstream

Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],

  1. soju must also update the database accordingly to upstreams' casemapping, otherwise it will end up inconsistent,
  2. soju must "normalize" entity names and expose only one casemapping that is a subset of all supported casemappings (here, ascii).

[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.

Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.

downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.

Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).

Message forwarding and casemapping

downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).

marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).

Casemapping changes

Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/irc.go

    r463 r478  
    122122                                }
    123123                                member := arguments[nextArgument]
    124                                 if _, ok := ch.Members[member]; ok {
     124                                m := ch.Members.Value(member)
     125                                if m != nil {
    125126                                        if plusMinus == '+' {
    126                                                 ch.Members[member].Add(ch.conn.availableMemberships, membership)
     127                                                m.Add(ch.conn.availableMemberships, membership)
    127128                                        } else {
    128129                                                // TODO: for upstreams without multi-prefix, query the user modes again
    129                                                 ch.Members[member].Remove(membership)
     130                                                m.Remove(membership)
    130131                                        }
    131132                                }
     
    419420        return cmd, params, true
    420421}
     422
     423type casemapping func(string) string
     424
     425func casemapNone(name string) string {
     426        return name
     427}
     428
     429// CasemapASCII of name is the canonical representation of name according to the
     430// ascii casemapping.
     431func casemapASCII(name string) string {
     432        var sb strings.Builder
     433        sb.Grow(len(name))
     434        for _, r := range name {
     435                if 'A' <= r && r <= 'Z' {
     436                        r += 'a' - 'A'
     437                }
     438                sb.WriteRune(r)
     439        }
     440        return sb.String()
     441}
     442
     443// casemapRFC1459 of name is the canonical representation of name according to the
     444// rfc1459 casemapping.
     445func casemapRFC1459(name string) string {
     446        var sb strings.Builder
     447        sb.Grow(len(name))
     448        for _, r := range name {
     449                if 'A' <= r && r <= 'Z' {
     450                        r += 'a' - 'A'
     451                } else if r == '{' {
     452                        r = '['
     453                } else if r == '}' {
     454                        r = ']'
     455                } else if r == '\\' {
     456                        r = '|'
     457                } else if r == '~' {
     458                        r = '^'
     459                }
     460                sb.WriteRune(r)
     461        }
     462        return sb.String()
     463}
     464
     465// casemapRFC1459Strict of name is the canonical representation of name
     466// according to the rfc1459-strict casemapping.
     467func casemapRFC1459Strict(name string) string {
     468        var sb strings.Builder
     469        sb.Grow(len(name))
     470        for _, r := range name {
     471                if 'A' <= r && r <= 'Z' {
     472                        r += 'a' - 'A'
     473                } else if r == '{' {
     474                        r = '['
     475                } else if r == '}' {
     476                        r = ']'
     477                } else if r == '\\' {
     478                        r = '|'
     479                }
     480                sb.WriteRune(r)
     481        }
     482        return sb.String()
     483}
     484
     485func parseCasemappingToken(tokenValue string) (casemap casemapping, ok bool) {
     486        switch tokenValue {
     487        case "ascii":
     488                casemap = casemapASCII
     489        case "rfc1459":
     490                casemap = casemapRFC1459
     491        case "rfc1459-strict":
     492                casemap = casemapRFC1459Strict
     493        default:
     494                return nil, false
     495        }
     496        return casemap, true
     497}
     498
     499func partialCasemap(higher casemapping, name string) string {
     500        nameFullyCM := higher(name)
     501        var sb strings.Builder
     502        sb.Grow(len(name))
     503        for i, r := range nameFullyCM {
     504                if 'a' <= r && r <= 'z' {
     505                        r = rune(name[i])
     506                }
     507                sb.WriteRune(r)
     508        }
     509        return sb.String()
     510}
     511
     512type casemapMap struct {
     513        innerMap map[string]casemapEntry
     514        casemap  casemapping
     515}
     516
     517type casemapEntry struct {
     518        originalKey string
     519        value       interface{}
     520}
     521
     522func newCasemapMap(size int) casemapMap {
     523        return casemapMap{
     524                innerMap: make(map[string]casemapEntry, size),
     525                casemap:  casemapNone,
     526        }
     527}
     528
     529func (cm *casemapMap) OriginalKey(name string) (key string, ok bool) {
     530        entry, ok := cm.innerMap[cm.casemap(name)]
     531        if !ok {
     532                return "", false
     533        }
     534        return entry.originalKey, true
     535}
     536
     537func (cm *casemapMap) Has(name string) bool {
     538        _, ok := cm.innerMap[cm.casemap(name)]
     539        return ok
     540}
     541
     542func (cm *casemapMap) Len() int {
     543        return len(cm.innerMap)
     544}
     545
     546func (cm *casemapMap) SetValue(name string, value interface{}) {
     547        nameCM := cm.casemap(name)
     548        entry, ok := cm.innerMap[nameCM]
     549        if !ok {
     550                cm.innerMap[nameCM] = casemapEntry{
     551                        originalKey: name,
     552                        value:       value,
     553                }
     554                return
     555        }
     556        entry.value = value
     557        cm.innerMap[nameCM] = entry
     558}
     559
     560func (cm *casemapMap) Delete(name string) {
     561        delete(cm.innerMap, cm.casemap(name))
     562}
     563
     564func (cm *casemapMap) SetCasemapping(newCasemap casemapping) {
     565        cm.casemap = newCasemap
     566        newInnerMap := make(map[string]casemapEntry, len(cm.innerMap))
     567        for _, entry := range cm.innerMap {
     568                newInnerMap[cm.casemap(entry.originalKey)] = entry
     569        }
     570        cm.innerMap = newInnerMap
     571}
     572
     573type upstreamChannelCasemapMap struct{ casemapMap }
     574
     575func (cm *upstreamChannelCasemapMap) Value(name string) *upstreamChannel {
     576        entry, ok := cm.innerMap[cm.casemap(name)]
     577        if !ok {
     578                return nil
     579        }
     580        return entry.value.(*upstreamChannel)
     581}
     582
     583type channelCasemapMap struct{ casemapMap }
     584
     585func (cm *channelCasemapMap) Value(name string) *Channel {
     586        entry, ok := cm.innerMap[cm.casemap(name)]
     587        if !ok {
     588                return nil
     589        }
     590        return entry.value.(*Channel)
     591}
     592
     593type membershipsCasemapMap struct{ casemapMap }
     594
     595func (cm *membershipsCasemapMap) Value(name string) *memberships {
     596        entry, ok := cm.innerMap[cm.casemap(name)]
     597        if !ok {
     598                return nil
     599        }
     600        return entry.value.(*memberships)
     601}
     602
     603type mapStringStringCasemapMap struct{ casemapMap }
     604
     605func (cm *mapStringStringCasemapMap) Value(name string) map[string]string {
     606        entry, ok := cm.innerMap[cm.casemap(name)]
     607        if !ok {
     608                return nil
     609        }
     610        return entry.value.(map[string]string)
     611}
Note: See TracChangeset for help on using the changeset viewer.