diff options
Diffstat (limited to 'libsmartcols/src/grouping.c')
-rw-r--r-- | libsmartcols/src/grouping.c | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/libsmartcols/src/grouping.c b/libsmartcols/src/grouping.c new file mode 100644 index 0000000..0b27cb2 --- /dev/null +++ b/libsmartcols/src/grouping.c @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2018 Karel Zak <kzak@redhat.com> + */ +#include "smartcolsP.h" + +/** + * SECTION: grouping + * @title: Grouping + * @short_description: lines grouing + * + * Lines groups manipulation API. The grouping API can be used 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 (addressed 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; +} |