of %{User-Name[#]} will yeild "1".
.IP %{Attribute-Name[*]}
-All instances of Attribute-Name, concatenated together with \\n as the
+All values of Attribute-Name, concatenated together with \\n as the
separator.
+.IP %{List-Name:[#]}
+The number of attributes in the named list.
+
+.IP %{List-Name:[*]}
+All values of attributes in the named-list, concatenated together with \\n
+as the separator. Use the %{pairs:} xlat to get a list of attributes and
+values.
+
e.g. If a response contains "Reply-Message = 'Hello', Reply-Message = 'bob'
the expnsion of "%{reply:Reply-Message[*]} will yield "Hello\\nbob"
return -(p - name);
}
- if (*p == '\0') {
+ attr.tag = TAG_ANY;
+ attr.num = NUM_ANY;
+
+ /*
+ * This may be just a bare list, but it can still
+ * have instance selectors and tag selectors.
+ */
+ switch (*p) {
+ case '\0':
type = TMPL_TYPE_LIST;
+ attr.num = NUM_ALL; /* Hack - Should be removed once tests are updated */
goto finish;
- }
- attr.tag = TAG_ANY;
- attr.num = NUM_ANY;
+ case '[':
+ type = TMPL_TYPE_LIST;
+ attr.num = NUM_ALL; /* Hack - Should be removed once tests are updated */
+ goto do_num;
+
+ default:
+ break;
+ }
attr.da = dict_attrbyname_substr(&p);
if (!attr.da) {
((DICT_ATTR *)&attr.unknown.da)->vendor);
if (attr.da) {
vpt->auto_converted = true;
- goto skip_tag;
+ goto do_num;
}
if (!allow_unknown) {
*/
attr.da = (DICT_ATTR *)&attr.unknown.da;
- goto skip_tag; /* unknown attributes can't have tags */
+ goto do_num; /* unknown attributes can't have tags */
}
/*
}
*q = '\0';
- goto skip_tag;
+ goto do_num;
}
/*
* The string MIGHT have a tag.
*/
if (*p == ':') {
- if (!attr.da->flags.has_tag) {
+ if (attr.da && !attr.da->flags.has_tag) { /* Lists don't have a da */
fr_strerror_printf("Attribute '%s' cannot have a tag", attr.da->name);
return -(p - name);
}
p = q;
}
-skip_tag:
+do_num:
if (*p == '\0') goto finish;
if (*p == '[') {
case TMPL_TYPE_XLAT_STRUCT:
c = '"';
break;
-
- case TMPL_TYPE_LIST:
case TMPL_TYPE_LITERAL: /* single-quoted or bare word */
/*
* Hack
VALUE_PAIR *tmpl_cursor_init(int *err, vp_cursor_t *cursor, REQUEST *request, vp_tmpl_t const *vpt)
{
VALUE_PAIR **vps, *vp = NULL;
+ int num;
VERIFY_TMPL(vpt);
* May not may not be found, but it *is* a known name.
*/
case TMPL_TYPE_ATTR:
- {
- int num;
-
switch (vpt->tmpl_num) {
case NUM_ANY:
vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
* Get the last instance of a VALUE_PAIR.
*/
case NUM_LAST:
-
{
VALUE_PAIR *last = NULL;
VERIFY_VP(vp);
last = vp;
}
-
+ VERIFY_VP(last);
if (!last) break;
return last;
}
* total number of attributes.
*/
case NUM_COUNT:
+ return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
+
default:
num = vpt->tmpl_num;
while ((vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag))) {
if (err) *err = -1;
return NULL;
- }
case TMPL_TYPE_LIST:
- vp = fr_cursor_init(cursor, vps);
+ switch (vpt->tmpl_num) {
+ case NUM_COUNT:
+ case NUM_ANY:
+ case NUM_ALL:
+ vp = fr_cursor_init(cursor, vps);
+ if (!vp) {
+ if (err) *err = -1;
+ return NULL;
+ }
+ VERIFY_VP(vp);
+ return vp;
+
+ /*
+ * Get the last instance of a VALUE_PAIR.
+ */
+ case NUM_LAST:
+ {
+ VALUE_PAIR *last = NULL;
+
+ for (vp = fr_cursor_init(cursor, vps);
+ vp;
+ vp = fr_cursor_next(cursor)) {
+ VERIFY_VP(vp);
+ last = vp;
+ }
+ if (!last) break;
+ VERIFY_VP(last);
+ return last;
+ }
+
+ default:
+ num = vpt->tmpl_num;
+ for (vp = fr_cursor_init(cursor, vps);
+ vp;
+ vp = fr_cursor_next(cursor)) {
+ VERIFY_VP(vp);
+ if (num-- <= 0) return vp;
+ }
+ break;
+ }
+
break;
default:
* May not may not be found, but it *is* a known name.
*/
case TMPL_TYPE_ATTR:
- if (vpt->tmpl_num != NUM_ALL) return NULL;
+ switch (vpt->tmpl_num) {
+ default:
+ return NULL;
+
+ case NUM_ALL:
+ case NUM_COUNT: /* This cursor is being used to count matching attrs */
+ break;
+ }
return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag);
case TMPL_TYPE_LIST:
+ switch (vpt->tmpl_num) {
+ default:
+ return NULL;
+
+ case NUM_ALL:
+ case NUM_COUNT: /* This cursor is being used to count matching attrs */
+ break;
+ }
return fr_cursor_next(cursor);
default:
RADIUS_PACKET *packet = NULL;
DICT_VALUE *dv;
char *ret = NULL;
- int err;
+ vp_cursor_t cursor;
char quote = escape ? '"' : '\0';
- vp_cursor_t cursor;
+ rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST));
+
+ /*
+ * We only support count and concatenate operations on lists.
+ */
+ if (vpt->type == TMPL_TYPE_LIST) {
+ vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
+ goto do_print;
+ }
/*
* See if we're dealing with an attribute in the request
* This allows users to manipulate virtual attributes as if
* they were real ones.
*/
- vp = tmpl_cursor_init(&err, &cursor, request, vpt);
+ vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
if (vp) goto do_print;
/*
{
int count = 0;
- fr_cursor_first(&cursor);
- while (fr_cursor_next_by_da(&cursor, vpt->tmpl_da, vpt->tmpl_tag)) count++;
+ for (vp = tmpl_cursor_init(NULL, &cursor, request, vpt);
+ vp;
+ vp = tmpl_cursor_next(&cursor, vpt)) count++;
return talloc_typed_asprintf(ctx, "%d", count);
}
--- /dev/null
+#
+# PRE: update
+#
+update control {
+ control !* ANY
+}
+
+update control {
+ Tmp-IP-Address-0 := 192.0.2.1
+ Tmp-IP-Address-0 += 192.0.2.2
+}
+
+if ("%{control:[#]}" != 2) {
+ update {
+ reply:Filter-Id += 'fail 0'
+ }
+}
+
+debug_control
+
+if (("%{control:[0]}" != 192.0.2.1) || ("%{control:[1]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if (("%{control:[n]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id += 'fail 1a'
+ }
+}
+
+if ("%{control:[*]}" != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+# Try calling these xlats in mapping too, they may get optimised to VPTs which is a
+# different code path.
+update request {
+ Tmp-IP-Address-1 += "%{control:[1]}"
+ Tmp-IP-Address-1 += "%{control:[0]}"
+ Tmp-String-0 = "%{control:[*]}"
+ Tmp-Integer-0 = "%{control:[#]}"
+}
+
+if (Tmp-String-0 != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+if (Tmp-Integer-0 != 2) {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
+
+# Boilerplate junk
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}