From 54f9b939ac239067cfc1a6d385b31c32b34f5b3f Mon Sep 17 00:00:00 2001 From: Michael Aldridge Date: Tue, 12 Nov 2019 23:17:34 -0800 Subject: [PATCH] Add index generation for caches --- .gitignore | 2 +- cache/cache.go | 34 ++++++++++++++++++++++++++++++++++ cache/cache_test.go | 33 +++++++++++++++++++++++++++++++++ cache/entries.go | 39 +++++++++++++++++++++++++++++++++++++++ cache/entries_test.go | 33 +++++++++++++++++++++++++++++++++ nsscache.go | 29 +++++++++++++++++++++++++---- nsscache_test.go | 8 ++++++++ 7 files changed, 173 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 32c23b6..8e1a6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -examples/*/*.cache +examples/*/*.cache* vendor/ diff --git a/cache/cache.go b/cache/cache.go index 33fa85e..e834b0d 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -2,7 +2,10 @@ package cache import ( + "bytes" + "fmt" "io" + "sort" ) type ACL func(e Entry) bool @@ -57,3 +60,34 @@ func (c *Cache) WriteTo(w io.Writer) (int64, error) { } return total, nil } + +// Index generates an index for the given cache on a particular +// column. This is required for caches beyond a libnss-cache defined +// size in order for them to be read correctly. +func (c *Cache) Index(col int) bytes.Buffer { + ordered := make([]string, len(c.entries)) + mapped := make(map[string]Entry, len(c.entries)) + for i := range c.entries { + key := c.entries[i].Column(col) + ordered[i] = key + mapped[key] = c.entries[i] + } + + // libnss-cache depends on the indexes being ordered in order + // to accelerate the system with a binary search. + sort.Strings(ordered) + + var b bytes.Buffer + var offset int64 + for _, key := range ordered { + b.WriteString(key) + b.WriteByte(0) + fmt.Fprintf(&b, "%08d", offset) + for i := 0; i < 32-len(key)-1; i++ { + b.WriteByte(0) + } + b.WriteString("\n") + offset += int64(len(mapped[key].String())) + 1 + } + return b +} diff --git a/cache/cache_test.go b/cache/cache_test.go index 198320a..c13e6cf 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -101,3 +101,36 @@ func TestCache_WriteTo(t *testing.T) { _, err := c.WriteTo(w) assert.NotNil(t, err) } + +func TestCacheIndex(t *testing.T) { + c := NewCache() + c.Add(&PasswdEntry{ + Name: "foo", + Passwd: "x", + UID: 1000, + GID: 1000, + GECOS: "Mr Foo", + Dir: "/home/foo", + Shell: "/bin/bash", + }, &PasswdEntry{ + Name: "admin", + Passwd: "x", + UID: 1002, + GID: 1000, + GECOS: "Admin", + Dir: "/home/admin", + Shell: "/bin/bash", + }, &PasswdEntry{ + Name: "bar", + Passwd: "x", + UID: 1001, + GID: 1000, + GECOS: "Mrs Bar", + Dir: "/home/bar", + Shell: "/bin/bash", + }) + + idx := c.Index(0) + expected := []byte{97, 100, 109, 105, 110, 0, 48, 48, 48, 48, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 98, 97, 114, 0, 48, 48, 48, 48, 48, 48, 52, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 102, 111, 111, 0, 48, 48, 48, 48, 48, 48, 57, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10} + assert.Equal(t, expected, idx.Bytes()) +} diff --git a/cache/entries.go b/cache/entries.go index 75cbc09..8f6c512 100644 --- a/cache/entries.go +++ b/cache/entries.go @@ -9,6 +9,8 @@ import ( type Entry interface { fmt.Stringer io.WriterTo + + Column(int) string } // PasswdEntry describes an entry of the /etc/passwd file @@ -44,6 +46,19 @@ func (e *PasswdEntry) args() []interface{} { } } +// Column returns the information from the requested columns or an +// empty string if no column is known. +func (e *PasswdEntry) Column(col int) string { + switch col { + case 0: + return e.Name + case 2: + return fmt.Sprintf("%d", e.UID) + default: + return "" + } +} + func (e *PasswdEntry) String() string { return fmt.Sprintf(e.format(), e.args()...) } @@ -89,6 +104,17 @@ func (e *ShadowEntry) args() []interface{} { } } +// Column returns the information from the requested columns or an +// empty string if no column is known. +func (e *ShadowEntry) Column(col int) string { + switch col { + case 0: + return e.Name + default: + return "" + } +} + func (e *ShadowEntry) String() string { return fmt.Sprintf(e.format(), e.args()...) } @@ -124,6 +150,19 @@ func (e *GroupEntry) args() []interface{} { } } +// Column returns the information from the requested columns or an +// empty string if no column is known. +func (e *GroupEntry) Column(col int) string { + switch col { + case 0: + return e.Name + case 2: + return fmt.Sprintf("%d", e.GID) + default: + return "" + } +} + func (e *GroupEntry) String() string { return fmt.Sprintf(e.format(), e.args()...) } diff --git a/cache/entries_test.go b/cache/entries_test.go index 297da24..b678de1 100644 --- a/cache/entries_test.go +++ b/cache/entries_test.go @@ -36,6 +36,21 @@ func TestPasswdEntry_WriteTo(t *testing.T) { assert.Equal(t, expected, b.String()) } +func TestPasswdEntry_Column(t *testing.T) { + e := PasswdEntry{ + Name: "foo", + UID: 1000, + GID: 1000, + GECOS: "Mr Foo", + Dir: "/home/foo", + Shell: "/usr/bin/bash", + } + + assert.Equal(t, "foo", e.Column(0)) + assert.Equal(t, "1000", e.Column(2)) + assert.Equal(t, "", e.Column(1)) +} + func TestShadowEntry_String(t *testing.T) { e := ShadowEntry{ Name: "foo", @@ -56,6 +71,14 @@ func TestShadowEntry_WriteTo(t *testing.T) { assert.Equal(t, expected, b.String()) } +func TestShadowEntry_Column(t *testing.T) { + e := ShadowEntry{ + Name: "foo", + } + assert.Equal(t, "foo", e.Column(0)) + assert.Equal(t, "", e.Column(1)) +} + func TestGroupEntry_String(t *testing.T) { e := GroupEntry{ Name: "foo", @@ -76,6 +99,16 @@ func TestGroupEntry_WriteTo(t *testing.T) { assert.Equal(t, expected, b.String()) } +func TestGroupEntry_Column(t *testing.T) { + e := GroupEntry{ + Name: "foo", + GID: 1000, + } + assert.Equal(t, "foo", e.Column(0)) + assert.Equal(t, "1000", e.Column(2)) + assert.Equal(t, "", e.Column(1)) +} + func writerToError(i int64, e error) error { return e } diff --git a/nsscache.go b/nsscache.go index f9631c1..d78cc15 100644 --- a/nsscache.go +++ b/nsscache.go @@ -3,7 +3,7 @@ package nsscache import ( "fmt" - "path" + "path/filepath" "os" @@ -75,7 +75,8 @@ func defaultWriteOptions() WriteOptions { } } -// WriteFiles write the content of the cache structs into files that libnss-cache can read +// WriteFiles write the content of the cache structs into files that +// libnss-cache can read. func (cm *CacheMap) WriteFiles(options *WriteOptions) error { wo := defaultWriteOptions() if options != nil { @@ -88,12 +89,32 @@ func (cm *CacheMap) WriteFiles(options *WriteOptions) error { } for _, name := range []string{"passwd", "shadow", "group"} { - filepath := path.Join(wo.Directory, fmt.Sprintf("%s.%s", name, wo.Extension)) + fpath := filepath.Join(wo.Directory, fmt.Sprintf("%s.%s", name, wo.Extension)) mode := 0644 if name == "shadow" { mode = 0000 } - if err := WriteAtomic(filepath, (*cm)[name], os.FileMode(mode)); err != nil { + if err := WriteAtomic(fpath, (*cm)[name], os.FileMode(mode)); err != nil { + return err + } + } + + idxCfg := []struct { + cache string + column int + supext string + }{ + {"passwd", 0, "ixname"}, + {"passwd", 2, "ixuid"}, + {"group", 0, "ixname"}, + {"group", 2, "ixgid"}, + {"shadow", 0, "ixname"}, + } + + for _, idx := range idxCfg { + fpath := filepath.Join(wo.Directory, fmt.Sprintf("%s.%s.%s", idx.cache, wo.Extension, idx.supext)) + idx := (*cm)[idx.cache].Index(idx.column) + if err := WriteAtomic(fpath, &idx, os.FileMode(0644)); err != nil { return err } } diff --git a/nsscache_test.go b/nsscache_test.go index f3bdeeb..7fc5a73 100644 --- a/nsscache_test.go +++ b/nsscache_test.go @@ -164,8 +164,16 @@ func TestCacheMap_WriteFiles(t *testing.T) { _, err = os.Stat(path.Join(dir, "passwd.cachetest")) assert.Nil(t, err) + _, err = os.Stat(path.Join(dir, "passwd.cachetest.ixname")) + assert.Nil(t, err) + _, err = os.Stat(path.Join(dir, "passwd.cachetest.ixuid")) + assert.Nil(t, err) _, err = os.Stat(path.Join(dir, "group.cachetest")) assert.Nil(t, err) + _, err = os.Stat(path.Join(dir, "group.cachetest.ixname")) + assert.Nil(t, err) + _, err = os.Stat(path.Join(dir, "group.cachetest.ixgid")) + assert.Nil(t, err) _, err = os.Stat(path.Join(dir, "shadow.cachetest")) assert.Nil(t, err)