feedbag_list.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. package toc
  2. import (
  3. "fmt"
  4. "math"
  5. "slices"
  6. "github.com/mk6i/open-oscar-server/state"
  7. "github.com/mk6i/open-oscar-server/wire"
  8. )
  9. // feedbagList provides operations for manipulating a collection of feedbag
  10. // items. It supports lookups by class/name/group, item insertion with
  11. // automatic ID generation, and transparent root group management.
  12. type feedbagList struct {
  13. items []*wire.FeedbagItem
  14. randInt func(int) int
  15. pendingUpdates []*wire.FeedbagItem
  16. pendingDeletes []*wire.FeedbagItem
  17. }
  18. // newFeedbagList creates a feedbagList from the given items. The randInt
  19. // function is used for generating unique item/group IDs; inject a
  20. // deterministic function in tests to assert exact feedbag item slices.
  21. func newFeedbagList(items []wire.FeedbagItem, randInt func(int) int) *feedbagList {
  22. ptrs := make([]*wire.FeedbagItem, len(items))
  23. for i := range items {
  24. ptrs[i] = &items[i]
  25. }
  26. return &feedbagList{
  27. items: ptrs,
  28. randInt: randInt,
  29. }
  30. }
  31. // SetMode upserts the permit/deny mode item.
  32. func (f *feedbagList) SetMode(mode uint8) {
  33. f.upsertItem(wire.FeedbagItem{
  34. ClassID: wire.FeedbagClassIdPdinfo,
  35. TLVLBlock: wire.TLVLBlock{
  36. TLVList: wire.TLVList{
  37. wire.NewTLVBE(wire.FeedbagAttributesPdMode, mode),
  38. },
  39. },
  40. })
  41. }
  42. // AddGroup returns the existing group with the given name or creates a new
  43. // one with an auto-generated GroupID. When a new group is created, the root
  44. // group's order TLV is updated to include it. Call PendingUpdates to retrieve
  45. // new or modified items for persistence. Returns the group item.
  46. func (f *feedbagList) AddGroup(name string) wire.FeedbagItem {
  47. if g := f.groupByName(name); g != nil {
  48. return *g
  49. }
  50. var root *wire.FeedbagItem
  51. for _, item := range f.items {
  52. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == 0 {
  53. root = item
  54. break
  55. }
  56. }
  57. if root == nil {
  58. root = &wire.FeedbagItem{ClassID: wire.FeedbagClassIdGroup, GroupID: 0}
  59. f.items = append(f.items, root)
  60. f.trackUpdate(root)
  61. }
  62. group := &wire.FeedbagItem{
  63. ClassID: wire.FeedbagClassIdGroup,
  64. Name: name,
  65. GroupID: f.genID(),
  66. }
  67. f.items = append(f.items, group)
  68. root.AppendOrderMembers(group.GroupID)
  69. f.trackUpdate(root)
  70. f.trackUpdate(group)
  71. return *group
  72. }
  73. // DeleteGroup marks a group item for deletion by name. If the group exists
  74. // and is not the root group, the root group's order TLV is updated.
  75. func (f *feedbagList) DeleteGroup(groupName string) {
  76. deleted, found := f.deleteItem(wire.FeedbagItem{
  77. Name: groupName,
  78. ClassID: wire.FeedbagClassIdGroup,
  79. })
  80. if found && deleted.GroupID > 0 {
  81. for _, item := range f.items {
  82. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == 0 {
  83. item.RemoveOrderMembers(deleted.GroupID)
  84. f.trackUpdate(item)
  85. }
  86. }
  87. }
  88. }
  89. // AddBuddy upserts a buddy item in the given group (by name), optionally
  90. // attaching alias and note attributes. Returns true if a new buddy was inserted.
  91. func (f *feedbagList) AddBuddy(groupName, screenName, alias, note string) (bool, error) {
  92. group := f.groupByName(groupName)
  93. if group == nil {
  94. return false, fmt.Errorf("group %q not found", groupName)
  95. }
  96. item := wire.FeedbagItem{
  97. ClassID: wire.FeedbagClassIdBuddy,
  98. GroupID: group.GroupID,
  99. Name: screenName,
  100. }
  101. if alias != "" {
  102. item.Append(wire.NewTLVBE(wire.FeedbagAttributesAlias, alias))
  103. }
  104. if note != "" {
  105. item.Append(wire.NewTLVBE(wire.FeedbagAttributesNote, note))
  106. }
  107. result, inserted := f.upsertItem(item)
  108. if inserted {
  109. group.AppendOrderMembers(result.ItemID)
  110. f.trackUpdate(group)
  111. }
  112. return inserted, nil
  113. }
  114. // DeleteBuddy marks a buddy item for deletion in the given group (by name).
  115. // The parent group's order TLV is updated to remove the buddy.
  116. func (f *feedbagList) DeleteBuddy(groupName, buddyName string) error {
  117. group := f.groupByName(groupName)
  118. if group == nil {
  119. return fmt.Errorf("group %q not found", groupName)
  120. }
  121. deleted, found := f.deleteItem(wire.FeedbagItem{
  122. ClassID: wire.FeedbagClassIdBuddy,
  123. GroupID: group.GroupID,
  124. Name: buddyName,
  125. })
  126. if found {
  127. group.RemoveOrderMembers(deleted.ItemID)
  128. f.trackUpdate(group)
  129. }
  130. return nil
  131. }
  132. // PermitUser upserts a permit-list entry for the given screen name.
  133. func (f *feedbagList) PermitUser(screenName string) {
  134. f.upsertItem(wire.FeedbagItem{
  135. ClassID: wire.FeedbagClassIDPermit,
  136. Name: screenName,
  137. })
  138. }
  139. // DenyUser upserts a deny-list entry for the given screen name.
  140. func (f *feedbagList) DenyUser(screenName string) {
  141. f.upsertItem(wire.FeedbagItem{
  142. ClassID: wire.FeedbagClassIDDeny,
  143. Name: screenName,
  144. })
  145. }
  146. // DeletePermit marks a permit-list entry for deletion.
  147. func (f *feedbagList) DeletePermit(screenName string) {
  148. f.deleteItem(wire.FeedbagItem{
  149. ClassID: wire.FeedbagClassIDPermit,
  150. Name: screenName,
  151. })
  152. }
  153. // DeleteDeny marks a deny-list entry for deletion.
  154. func (f *feedbagList) DeleteDeny(screenName string) {
  155. f.deleteItem(wire.FeedbagItem{
  156. ClassID: wire.FeedbagClassIDDeny,
  157. Name: screenName,
  158. })
  159. }
  160. // PendingUpdates returns items that were explicitly upserted via upsertItem
  161. // and items that were implicitly created or modified as side effects of other
  162. // operations (e.g., group order updates from upsertItem, root group updates
  163. // from AddGroup). The pending list is cleared after each call.
  164. func (f *feedbagList) PendingUpdates() []wire.FeedbagItem {
  165. var result []wire.FeedbagItem
  166. for _, p := range f.pendingUpdates {
  167. result = append(result, *p)
  168. }
  169. f.pendingUpdates = nil
  170. if len(result) == 0 {
  171. return nil
  172. }
  173. return result
  174. }
  175. // PendingDeletes returns items marked for deletion via deleteItem.
  176. // The pending list is cleared after each call.
  177. func (f *feedbagList) PendingDeletes() []wire.FeedbagItem {
  178. var result []wire.FeedbagItem
  179. for _, p := range f.pendingDeletes {
  180. result = append(result, *p)
  181. }
  182. f.pendingDeletes = nil
  183. return result
  184. }
  185. // groupByName returns the group item with the given name, or nil if not found.
  186. func (f *feedbagList) groupByName(name string) *wire.FeedbagItem {
  187. for _, item := range f.items {
  188. if item.ClassID == wire.FeedbagClassIdGroup && item.Name == name {
  189. return item
  190. }
  191. }
  192. return nil
  193. }
  194. // trackUpdate adds item to the pending-updates list if not already present.
  195. func (f *feedbagList) trackUpdate(item *wire.FeedbagItem) {
  196. if slices.Contains(f.pendingUpdates, item) {
  197. return // already tracked
  198. }
  199. f.pendingUpdates = append(f.pendingUpdates, item)
  200. }
  201. // itemsMatch reports whether two feedbag items are considered the same for
  202. // upsert/delete (buddy: ClassID, Name, GroupID; others: ClassID and Name).
  203. // Stored items are assumed to have normalized names; the input (b) name is
  204. // normalized for comparison when the class is buddy, permit, or deny.
  205. func (f *feedbagList) itemsMatch(a, b *wire.FeedbagItem) bool {
  206. if a.ClassID != b.ClassID {
  207. return false
  208. }
  209. var nameMatch bool
  210. if hasScreenName(a.ClassID) {
  211. nameMatch = a.Name == state.NewIdentScreenName(b.Name).String()
  212. } else {
  213. nameMatch = a.Name == b.Name
  214. }
  215. if !nameMatch {
  216. return false
  217. }
  218. if a.ClassID == wire.FeedbagClassIdBuddy {
  219. return a.GroupID == b.GroupID
  220. }
  221. return true
  222. }
  223. // deleteItem removes the first item matching the same criteria as upsertItem:
  224. // buddy items by ClassID, Name, and GroupID; other items by ClassID and Name.
  225. // Returns the deleted item and true if found, or a zero item and false otherwise.
  226. func (f *feedbagList) deleteItem(item wire.FeedbagItem) (wire.FeedbagItem, bool) {
  227. for i, existing := range f.items {
  228. if f.itemsMatch(existing, &item) {
  229. f.pendingDeletes = append(f.pendingDeletes, existing)
  230. f.items = append(f.items[:i], f.items[i+1:]...)
  231. return *existing, true
  232. }
  233. }
  234. return wire.FeedbagItem{}, false
  235. }
  236. // upsertItem updates an existing feedbag item or inserts a new one. Buddy
  237. // items are matched by GroupID, ClassID, and Name; all other items are matched
  238. // by ClassID and Name. When matched, the existing item is replaced in place
  239. // (preserving its ItemID). When no match is found, a new item is inserted with
  240. // an auto-generated ItemID. Names for buddy, permit, and deny items are
  241. // normalized before storage. Returns the stored item and true if a new item
  242. // was inserted, or the existing item and false if it was updated/unchanged.
  243. func (f *feedbagList) upsertItem(item wire.FeedbagItem) (wire.FeedbagItem, bool) {
  244. if hasScreenName(item.ClassID) {
  245. item.Name = state.NewIdentScreenName(item.Name).String() // normalize name
  246. }
  247. for _, existing := range f.items {
  248. if f.itemsMatch(existing, &item) {
  249. if !existing.IsEqual(item) {
  250. item.ItemID = existing.ItemID
  251. *existing = item
  252. f.trackUpdate(existing)
  253. }
  254. return *existing, false
  255. }
  256. }
  257. item.ItemID = f.genID()
  258. f.items = append(f.items, &item)
  259. f.pendingUpdates = append(f.pendingUpdates, &item)
  260. return item, true
  261. }
  262. // genID generates a unique ID that does not conflict with any existing ItemID
  263. // or GroupID in the list.
  264. func (f *feedbagList) genID() uint16 {
  265. num := uint16(f.randInt(math.MaxUint16))
  266. for itemID := num; itemID != num-1; itemID++ {
  267. if itemID == 0 {
  268. continue
  269. }
  270. exists := false
  271. for _, item := range f.items {
  272. if item.GroupID == itemID || item.ItemID == itemID {
  273. exists = true
  274. break
  275. }
  276. }
  277. if !exists {
  278. return itemID
  279. }
  280. }
  281. return 0
  282. }
  283. // hasScreenName reports whether the feedbag class stores a screen name that
  284. // should be normalized (buddy, permit, deny).
  285. func hasScreenName(classID uint16) bool {
  286. return classID == wire.FeedbagClassIdBuddy ||
  287. classID == wire.FeedbagClassIDPermit ||
  288. classID == wire.FeedbagClassIDDeny
  289. }