/* * Copyright (C) 2018 Karel Zak */ #include "smartcolsP.h" /** * SECTION: grouping * @title: Grouping * @short_description: lines grouing * * Lines groups manipulation API. The grouping API allows to create M:N * relations between lines and on tree-like output it prints extra chart to * visualize these relations. The group has unlimited number of members and * group childs. See libsmartcols/sample/grouping* for more details. */ /* Private API */ void scols_ref_group(struct libscols_group *gr) { if (gr) gr->refcount++; } void scols_group_remove_children(struct libscols_group *gr) { if (!gr) return; while (!list_empty(&gr->gr_children)) { struct libscols_line *ln = list_entry(gr->gr_children.next, struct libscols_line, ln_children); DBG(GROUP, ul_debugobj(gr, "remove child")); list_del_init(&ln->ln_children); scols_ref_group(ln->parent_group); ln->parent_group = NULL; scols_unref_line(ln); } } void scols_group_remove_members(struct libscols_group *gr) { if (!gr) return; while (!list_empty(&gr->gr_members)) { struct libscols_line *ln = list_entry(gr->gr_members.next, struct libscols_line, ln_groups); DBG(GROUP, ul_debugobj(gr, "remove member [%p]", ln)); list_del_init(&ln->ln_groups); scols_unref_group(ln->group); ln->group->nmembers++; ln->group = NULL; scols_unref_line(ln); } } /* note group has to be already without members to deallocate */ void scols_unref_group(struct libscols_group *gr) { if (gr && --gr->refcount <= 0) { DBG(GROUP, ul_debugobj(gr, "dealloc")); scols_group_remove_children(gr); list_del(&gr->gr_groups); free(gr); return; } } static void groups_fix_members_order(struct libscols_line *ln) { struct libscols_iter itr; struct libscols_line *child; if (ln->group) { INIT_LIST_HEAD(&ln->ln_groups); list_add_tail(&ln->ln_groups, &ln->group->gr_members); DBG(GROUP, ul_debugobj(ln->group, "fixing member line=%p [%zu/%zu]", ln, ln->group->nmembers, list_count_entries(&ln->group->gr_members))); } scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_line_next_child(ln, &itr, &child) == 0) groups_fix_members_order(child); /* * We modify gr_members list, so is_last_group_member() does not have * to provide reliable answer, we need to verify by list_count_entries(). */ if (ln->group && is_last_group_member(ln) && ln->group->nmembers == list_count_entries(&ln->group->gr_members)) { DBG(GROUP, ul_debugobj(ln->group, "fixing childs")); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_line_next_group_child(ln, &itr, &child) == 0) groups_fix_members_order(child); } } void scols_groups_fix_members_order(struct libscols_table *tb) { struct libscols_iter itr; struct libscols_line *ln; struct libscols_group *gr; /* remove all from groups lists */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_group(tb, &itr, &gr) == 0) { while (!list_empty(&gr->gr_members)) { struct libscols_line *line = list_entry(gr->gr_members.next, struct libscols_line, ln_groups); list_del_init(&line->ln_groups); } } /* add again to the groups list in order we walk in tree */ scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_line(tb, &itr, &ln) == 0) { if (ln->parent || ln->parent_group) continue; groups_fix_members_order(ln); } /* If group child is member of another group * scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_group(tb, &itr, &gr) == 0) { struct libscols_iter xitr; struct libscols_line *child; scols_reset_iter(&xitr, SCOLS_ITER_FORWARD); while (scols_line_next_group_child(ln, &xitr, &child) == 0) groups_fix_members_order(child); } */ } static inline const char *group_state_to_string(int state) { static const char *grpstates[] = { [SCOLS_GSTATE_NONE] = "none", [SCOLS_GSTATE_FIRST_MEMBER] = "1st-member", [SCOLS_GSTATE_MIDDLE_MEMBER] = "middle-member", [SCOLS_GSTATE_LAST_MEMBER] = "last-member", [SCOLS_GSTATE_MIDDLE_CHILD] = "middle-child", [SCOLS_GSTATE_LAST_CHILD] = "last-child", [SCOLS_GSTATE_CONT_MEMBERS] = "continue-members", [SCOLS_GSTATE_CONT_CHILDREN] = "continue-children" }; assert(state >= 0); assert((size_t) state < ARRAY_SIZE(grpstates)); return grpstates[state]; } /* static void grpset_debug(struct libscols_table *tb, struct libscols_line *ln) { size_t i; for (i = 0; i < tb->grpset_size; i++) { if (tb->grpset[i]) { struct libscols_group *gr = tb->grpset[i]; if (ln) DBG(LINE, ul_debugobj(ln, "grpset[%zu]: %p %s", i, gr, group_state_to_string(gr->state))); else DBG(LINE, ul_debug("grpset[%zu]: %p %s", i, gr, group_state_to_string(gr->state))); } else if (ln) { DBG(LINE, ul_debugobj(ln, "grpset[%zu]: free", i)); } else DBG(LINE, ul_debug("grpset[%zu]: free", i)); } } */ static int group_state_for_line(struct libscols_group *gr, struct libscols_line *ln) { if (gr->state == SCOLS_GSTATE_NONE && (ln->group != gr || !is_first_group_member(ln))) /* * NONE is possible to translate to FIRST_MEMBER only, and only if * line group matches with the current group. */ return SCOLS_GSTATE_NONE; if (ln->group != gr && ln->parent_group != gr) { /* Not our line, continue */ if (gr->state == SCOLS_GSTATE_FIRST_MEMBER || gr->state == SCOLS_GSTATE_MIDDLE_MEMBER || gr->state == SCOLS_GSTATE_CONT_MEMBERS) return SCOLS_GSTATE_CONT_MEMBERS; if (gr->state == SCOLS_GSTATE_LAST_MEMBER || gr->state == SCOLS_GSTATE_MIDDLE_CHILD || gr->state == SCOLS_GSTATE_CONT_CHILDREN) return SCOLS_GSTATE_CONT_CHILDREN; } else if (ln->group == gr && is_first_group_member(ln)) { return SCOLS_GSTATE_FIRST_MEMBER; } else if (ln->group == gr && is_last_group_member(ln)) { return SCOLS_GSTATE_LAST_MEMBER; } else if (ln->group == gr && is_group_member(ln)) { return SCOLS_GSTATE_MIDDLE_MEMBER; } else if (ln->parent_group == gr && is_last_group_child(ln)) { return SCOLS_GSTATE_LAST_CHILD; } else if (ln->parent_group == gr && is_group_child(ln)) { return SCOLS_GSTATE_MIDDLE_CHILD; } return SCOLS_GSTATE_NONE; } /* * apply new @state to the chunk (addresesd by @xx) of grpset used for the group (@gr) */ static void grpset_apply_group_state(struct libscols_group **xx, int state, struct libscols_group *gr) { size_t i; DBG(GROUP, ul_debugobj(gr, " applying state to grpset")); /* gr->state holds the old state, @state is the new state */ for (i = 0; i < SCOLS_GRPSET_CHUNKSIZ; i++) xx[i] = state == SCOLS_GSTATE_NONE ? NULL : gr; gr->state = state; } static struct libscols_group **grpset_locate_freespace(struct libscols_table *tb, int chunks, int prepend) { size_t i, avail = 0; struct libscols_group **tmp, **first = NULL; const size_t wanted = chunks * SCOLS_GRPSET_CHUNKSIZ; if (!tb->grpset_size) prepend = 0; /* DBG(TAB, ul_debugobj(tb, "orig grpset:")); grpset_debug(tb, NULL); */ if (prepend) { for (i = tb->grpset_size - 1; ; i--) { if (tb->grpset[i] == NULL) { first = &tb->grpset[i]; avail++; } else avail = 0; if (avail == wanted) goto done; if (i == 0) break; } } else { for (i = 0; i < tb->grpset_size; i++) { if (tb->grpset[i] == NULL) { if (avail == 0) first = &tb->grpset[i]; avail++; } else avail = 0; if (avail == wanted) goto done; } } DBG(TAB, ul_debugobj(tb, " realocate grpset [sz: old=%zu, new=%zu, new_chunks=%d]", tb->grpset_size, tb->grpset_size + wanted, chunks)); tmp = realloc(tb->grpset, (tb->grpset_size + wanted) * sizeof(struct libscols_group *)); if (!tmp) return NULL; tb->grpset = tmp; if (prepend) { DBG(TAB, ul_debugobj(tb, " prepending free space")); char *dest = (char *) tb->grpset; memmove( dest + (wanted * sizeof(struct libscols_group *)), tb->grpset, tb->grpset_size * sizeof(struct libscols_group *)); first = tb->grpset; } else { first = tb->grpset + tb->grpset_size; } memset(first, 0, wanted * sizeof(struct libscols_group *)); tb->grpset_size += wanted; done: /* DBG(TAB, ul_debugobj(tb, "new grpset:")); grpset_debug(tb, NULL); */ return first; } static struct libscols_group **grpset_locate_group(struct libscols_table *tb, struct libscols_group *gr) { size_t i; for (i = 0; i < tb->grpset_size; i++) { if (gr == tb->grpset[i]) return &tb->grpset[i]; } return NULL; } static int grpset_update(struct libscols_table *tb, struct libscols_line *ln, struct libscols_group *gr) { struct libscols_group **xx; int state; DBG(LINE, ul_debugobj(ln, " group [%p] grpset update [grpset size=%zu]", gr, tb->grpset_size)); /* new state, note that gr->state still holds the original state */ state = group_state_for_line(gr, ln); DBG(LINE, ul_debugobj(ln, " state %s --> %s", group_state_to_string(gr->state), group_state_to_string(state))); if (state == SCOLS_GSTATE_FIRST_MEMBER && gr->state != SCOLS_GSTATE_NONE) { DBG(LINE, ul_debugobj(ln, "wrong group initialization (%s)", group_state_to_string(gr->state))); abort(); } if (state != SCOLS_GSTATE_NONE && gr->state == SCOLS_GSTATE_LAST_CHILD) { DBG(LINE, ul_debugobj(ln, "wrong group termination (%s)", group_state_to_string(gr->state))); abort(); } if (gr->state == SCOLS_GSTATE_LAST_MEMBER && !(state == SCOLS_GSTATE_LAST_CHILD || state == SCOLS_GSTATE_CONT_CHILDREN || state == SCOLS_GSTATE_MIDDLE_CHILD || state == SCOLS_GSTATE_NONE)) { DBG(LINE, ul_debugobj(ln, "wrong group member->child order")); abort(); } /* should not happen; probably wrong line... */ if (gr->state == SCOLS_GSTATE_NONE && state == SCOLS_GSTATE_NONE) return 0; /* locate place in grpset where we draw the group */ if (!tb->grpset || gr->state == SCOLS_GSTATE_NONE) xx = grpset_locate_freespace(tb, 1, 1); else xx = grpset_locate_group(tb, gr); if (!xx) { DBG(LINE, ul_debugobj(ln, "failed to locate group or reallocate grpset")); return -ENOMEM; } grpset_apply_group_state(xx, state, gr); /*ON_DBG(LINE, grpset_debug(tb, ln));*/ return 0; } static int grpset_update_active_groups(struct libscols_table *tb, struct libscols_line *ln) { int rc = 0; size_t i; struct libscols_group *last = NULL; DBG(LINE, ul_debugobj(ln, " update for active groups")); for (i = 0; i < tb->grpset_size; i++) { struct libscols_group *gr = tb->grpset[i]; if (!gr || last == gr) continue; last = gr; rc = grpset_update(tb, ln, gr); if (rc) break; } DBG(LINE, ul_debugobj(ln, " <- active groups updated [rc=%d]", rc)); return rc; } int scols_groups_update_grpset(struct libscols_table *tb, struct libscols_line *ln) { int rc = 0; DBG(LINE, ul_debugobj(ln, " grpset update [line: group=%p, parent_group=%p", ln->group, ln->parent_group)); rc = grpset_update_active_groups(tb, ln); if (!rc && ln->group && ln->group->state == SCOLS_GSTATE_NONE) { DBG(LINE, ul_debugobj(ln, " introduce a new group")); rc = grpset_update(tb, ln, ln->group); } return rc; } void scols_groups_reset_state(struct libscols_table *tb) { struct libscols_iter itr; struct libscols_group *gr; DBG(TAB, ul_debugobj(tb, "reset groups states")); scols_reset_iter(&itr, SCOLS_ITER_FORWARD); while (scols_table_next_group(tb, &itr, &gr) == 0) { DBG(GROUP, ul_debugobj(gr, " reset to NONE")); gr->state = SCOLS_GSTATE_NONE; } if (tb->grpset) { DBG(TAB, ul_debugobj(tb, " zeroize grpset")); memset(tb->grpset, 0, tb->grpset_size * sizeof(struct libscols_group *)); } tb->ngrpchlds_pending = 0; } static void add_member(struct libscols_group *gr, struct libscols_line *ln) { DBG(GROUP, ul_debugobj(gr, "add member %p", ln)); ln->group = gr; gr->nmembers++; scols_ref_group(gr); INIT_LIST_HEAD(&ln->ln_groups); list_add_tail(&ln->ln_groups, &gr->gr_members); scols_ref_line(ln); } /* * Returns first group which is ready to print group children. * * This function scans grpset[] in backward order and returns first group * with SCOLS_GSTATE_CONT_CHILDREN or SCOLS_GSTATE_LAST_MEMBER state. */ struct libscols_group *scols_grpset_get_printable_children(struct libscols_table *tb) { size_t i; for (i = tb->grpset_size; i > 0; i -= SCOLS_GRPSET_CHUNKSIZ) { struct libscols_group *gr = tb->grpset[i-1]; if (gr == NULL) continue; if (gr->state == SCOLS_GSTATE_CONT_CHILDREN || gr->state == SCOLS_GSTATE_LAST_MEMBER) return gr; } return NULL; } /** * scols_table_group_lines: * @tb: a pointer to a struct libscols_table instance * @ln: new group member * @member: group member * @id: group identifier (unused, not implemented yet), use zero. * * This function add line @ln to group of lines represented by @member. If the * group is not yet defined (@member is not member of any group) than a new one * is allocated. * * The @ln maybe a NULL -- in this case only a new group is allocated if not * defined yet. * * Note that the same line cannot be member of more groups (not implemented * yet). The child of any group can be member of another group. * * The @id is not used for now, use 0. The plan is to use it to support * multi-group membership in future. * * Returns: 0, a negative value in case of an error. * * Since: 2.34 */ int scols_table_group_lines( struct libscols_table *tb, struct libscols_line *ln, struct libscols_line *member, __attribute__((__unused__)) int id) { struct libscols_group *gr = NULL; if (!tb || !member) { DBG(GROUP, ul_debugobj(gr, "failed group lines (no table or member)")); return -EINVAL; } if (ln) { if (ln->group && !member->group) { DBG(GROUP, ul_debugobj(gr, "failed group lines (new group, line member of another)")); return -EINVAL; } if (ln->group && member->group && ln->group != member->group) { DBG(GROUP, ul_debugobj(gr, "failed group lines (groups mismatch bwteen member and line")); return -EINVAL; } } gr = member->group; /* create a new group */ if (!gr) { gr = calloc(1, sizeof(*gr)); if (!gr) return -ENOMEM; DBG(GROUP, ul_debugobj(gr, "alloc")); gr->refcount = 1; INIT_LIST_HEAD(&gr->gr_members); INIT_LIST_HEAD(&gr->gr_children); INIT_LIST_HEAD(&gr->gr_groups); /* add group to the table */ list_add_tail(&gr->gr_groups, &tb->tb_groups); /* add the first member */ add_member(gr, member); } /* add to group */ if (ln && !ln->group) add_member(gr, ln); return 0; } /** * scols_line_link_group: * @ln: line instance * @member: group member * @id: group identifier (unused, not implemented yet)) * * Define @ln as child of group represented by group @member. The line @ln * cannot be child of any other line. It's possible to create group->child or * parent->child relationship, but no both for the same line (child). * * The @id is not used for now, use 0. The plan is to use it to support * multi-group membership in future. * * Returns: 0, a negative value in case of an error. * * Since: 2.34 */ int scols_line_link_group(struct libscols_line *ln, struct libscols_line *member, __attribute__((__unused__)) int id) { if (!ln || !member || !member->group || ln->parent) return -EINVAL; if (!list_empty(&ln->ln_children)) return -EINVAL; DBG(GROUP, ul_debugobj(member->group, "add child")); list_add_tail(&ln->ln_children, &member->group->gr_children); scols_ref_line(ln); ln->parent_group = member->group; scols_ref_group(member->group); return 0; }