1 /*******************************************************************************
2 
3     An identifier as defined by collectd
4 
5     An Identifier is of the form 'host/plugin-instance/type-instance'.
6     Both '-instance' parts are optional.
7     'plugin' and each '-instance' part may be chosen freely as long as
8     the tuple (plugin, plugin instance, type instance) uniquely identifies
9     the plugin within collectd.
10     'type' identifies the type and number of values (i. e. data-set)
11     passed to collectd.
12     A large list of predefined data-sets is available in the types.db file.
13 
14     See_Also:
15         https://collectd.org/documentation/manpages/collectd-unixsock.5.shtml
16         https://collectd.org/documentation/manpages/types.db.5.shtml
17 
18     Copyright:
19         Copyright (c) 2015-2016 dunnhumby Germany GmbH.
20         All rights reserved.
21 
22     License:
23         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
24         Alternatively, this file may be distributed under the terms of the Tango
25         3-Clause BSD License (see LICENSE_BSD.txt for details).
26 
27 *******************************************************************************/
28 
29 module ocean.net.collectd.Identifier;
30 
31 
32 
33 import ocean.meta.types.Qualifiers;
34 import ocean.core.ExceptionDefinitions; // IllegalArgumentException
35 import ocean.text.convert.Formatter;
36 import ocean.text.util.StringSearch; // locateChar
37 
38 version (unittest)
39 {
40     import ocean.core.Test;
41 }
42 
43 /// Convenience alias
44 private alias StringSearch!(false).locateChar locateChar;
45 
46 
47 
48 /******************************************************************************/
49 
50 public struct Identifier
51 {
52     /***************************************************************************
53 
54         Hostname
55 
56         If null, inferred from a call to hostname. This call will only take
57         place once, at application startup, so it won't have any
58         performance impact on running applications.
59         It is recommended to use null.
60 
61     ***************************************************************************/
62 
63     public cstring host;
64 
65 
66     /***************************************************************************
67 
68         Should be set to the application name
69 
70     ***************************************************************************/
71 
72     public cstring plugin;
73 
74 
75     /***************************************************************************
76 
77         Application defined type
78 
79         Types are defined in the file 'types.db'.
80         Each type correspond to a set of values, with a particular meaning.
81         For example, the type that would be used to log repeated invocation
82         of the 'df' command line utility would be a data type consisting of
83         two fields, 'used' and 'free'.
84 
85     ***************************************************************************/
86 
87     public cstring type;
88 
89 
90     /***************************************************************************
91 
92         Application instance
93 
94         It is recommended to use fixed numbers, such as [ 1, 2, 3, 4 ] if
95         you have, for example 4 instances of the application running
96         on the same 'host'.
97         You can also use names for an instance, however, the identifier should
98         be unique for this 'host', and should not change for the lifetime of
99         the application (so using PID as instance is discouraged for example);
100 
101     ***************************************************************************/
102 
103     public cstring plugin_instance;
104 
105 
106     /***********************************************************************
107 
108         Application defined instance of the category ('type')
109 
110         Each 'type' uniquely identify a kind of data, while 'type_instance'
111         link this to an instance of the data. For example, to continue
112         with the 'df' example (see 'type' documentation), 'type_instance'
113         would probably be set to mount points, such as 'dev', 'run', 'boot'.
114 
115     ***********************************************************************/
116 
117     public cstring type_instance;
118 
119 
120     /***********************************************************************
121 
122         Sanity checks
123 
124     ***********************************************************************/
125 
126     invariant ()
127     {
128         assert(this.host.length, "No host for identifier");
129         assert(this.plugin.length, "No plugin for identifier");
130         assert(this.type.length, "No type for identifier");
131     }
132 
133 
134     /***************************************************************************
135 
136         Convenience wrapper around `Identifier.create(cstring, Identifier)`
137         that throws on parsing error.
138 
139         Params:
140             line = An string matching 'host/plugin-instance/type-instance'.
141                    Both `instance` part are optionals, in which case the '-'
142                    should be omitted.
143 
144         Throws:
145             If the argument `line` is not a valid identifier.
146 
147         Returns:
148             The parsed identifier.
149 
150     ***************************************************************************/
151 
152     public static Identifier create (cstring line)
153     {
154         Identifier ret = void; // 'out' params are default initialized
155         if (auto msg = Identifier.create(line, ret))
156         {
157             // Because the ctor doesn't expose it...
158             auto e = new IllegalArgumentException(format("{}: {}", line, msg));
159             e.file = __FILE__;
160             e.line = __LINE__;
161             throw e;
162         }
163         return ret;
164     }
165 
166     unittest
167     {
168         testThrown!(IllegalArgumentException)(Identifier.create(""));
169         testThrown!(IllegalArgumentException)(Identifier.create("/"));
170         testThrown!(IllegalArgumentException)(Identifier.create("//"));
171         testThrown!(IllegalArgumentException)(Identifier.create("a/b/-"));
172         testThrown!(IllegalArgumentException)(Identifier.create("a/-/c"));
173         testThrown!(IllegalArgumentException)(Identifier.create("a/b-/c"));
174         testThrown!(IllegalArgumentException)(Identifier.create("a/b-/c-"));
175         testThrown!(IllegalArgumentException)(Identifier.create("a/b/c/d"));
176 
177         Identifier expected = { host: "a", plugin: "b", type: "c" };
178         testStructEquality(Identifier.create("a/b/c"), expected);
179 
180         expected.plugin_instance = "foo";
181         testStructEquality(Identifier.create("a/b-foo/c"), expected);
182 
183         expected.type_instance = "bar";
184         testStructEquality(Identifier.create("a/b-foo/c-bar"), expected);
185 
186         expected.type = "com.sociomantic.bytes_sent";
187         testStructEquality(
188             Identifier.create("a/b-foo/com.sociomantic.bytes_sent-bar"),
189             expected);
190     }
191 
192 
193     /***************************************************************************
194 
195         Parse a string and returns the corresponding `Identifier`
196 
197         If the string passed is not a valid identifier, this function will
198         return the reason why, else it returns `null`.
199 
200         This function is useful to get an identifier out of Collectd, or
201         from any Collectd-formatted identifier.
202         To construct an identifier with known values, initializing the fields
203         is enough.
204 
205         Params:
206             line = An string matching 'host/plugin-instance/type-instance'.
207                    Both `instance` part are optionals, in which case the '-'
208                    should be omitted.
209             identifier = An identifier to fill with the parsed string.
210                          If this function returns non-null, the state of
211                          `identifier` should not be relied upon.
212 
213         Returns:
214             `null` if the parsing succeeded, else a string representing the
215             error.
216 
217     ***************************************************************************/
218 
219     public static istring create (cstring line, out Identifier identifier)
220     {
221         if (!line.length)
222             return "Empty string is not a valid identifier";
223 
224         // Parses the host
225         {
226             auto slash = locateChar(line, '/');
227             identifier.host = line[0 .. slash];
228             line = line[slash + 1 .. $];
229         }
230 
231         // Parses the plugin and the optional instance
232         {
233             auto slash = locateChar(line, '/');
234             if (slash >= line.length)
235                 return "No plugin name found";
236             auto dash = locateChar(line[0 .. slash], '-');
237             if (dash < slash)
238             {
239                 identifier.plugin = line[0 .. dash];
240                 identifier.plugin_instance = line[dash + 1 .. slash];
241                 if (auto m = check!("plugin instance")(identifier.plugin_instance))
242                     return m;
243             }
244             else
245             {
246                 identifier.plugin = line[0 .. slash];
247                 identifier.plugin_instance = null;
248             }
249             line = line[slash + 1 .. $];
250         }
251 
252         // Finally, parses the type and the optional instance
253         {
254             auto dash = locateChar(line, '-');
255             if (!line.length)
256                 return "Empty type found";
257             if (dash < line.length)
258             {
259                 identifier.type = line[0 .. dash];
260                 identifier.type_instance = line[dash + 1 .. $];
261                 if (auto m = check!("type instance")(identifier.type_instance))
262                     return m;
263             }
264             else
265             {
266                 identifier.type = line;
267                 identifier.type_instance = null;
268             }
269         }
270 
271         // We don't check host for validity, only that it contains something
272         if (!identifier.host.length)
273             return "Empty host found";
274         if (auto msg = check!("plugin")(identifier.plugin))
275             return msg;
276         if (auto msg = check!("type")(identifier.type))
277             return msg;
278 
279         return null;
280     }
281 
282 
283     version (unittest)
284     {
285         /***********************************************************************
286 
287             Replacement for `test!("==")(identifierA, identifierB)`, as it would
288             trigger an assertion failure when formatting the arguments.
289 
290         ***********************************************************************/
291 
292         private static void testStructEquality (Identifier actual,
293                                                 Identifier expected)
294         {
295             test!("==")(actual.host, expected.host);
296             test!("==")(actual.plugin, expected.plugin);
297             test!("==")(actual.type, expected.type);
298             test!("==")(actual.plugin_instance, expected.plugin_instance);
299             test!("==")(actual.type_instance, expected.type_instance);
300         }
301     }
302 
303     /***************************************************************************
304 
305         Helper function template for validating a field's content
306 
307     ***************************************************************************/
308 
309     private static istring check (istring fieldname) (cstring field)
310     {
311         if (!field.length)
312             return "Empty " ~ fieldname ~ " found";
313 
314         auto idx = invalidIndex(field);
315         if (field.length != idx)
316             return "Invalid char found in " ~ fieldname
317                 ~ ", allowed chars are: [0-9][a-z][A-Z][._-]";
318         return null;
319     }
320 
321 
322     /***************************************************************************
323 
324         Find the first forbidden char in an identifier, if any
325 
326         Valid identifier are solely composed of [a-z][A-Z][0-9][._-]
327         If any char isn't in that list, this function returns its index.
328 
329         Params:
330             str = String to validate
331 
332         Returns:
333             The index at which the invalid char is, or `str.length` if there
334             isn't any
335 
336     ***************************************************************************/
337 
338     public static size_t invalidIndex (cstring str)
339     {
340         foreach (idx, c; str)
341         {
342             if (!(c >= 'a' && c <= 'z')
343                 && !(c >= 'A' && c <= 'Z')
344                 && !(c >= '0' && c <= '9')
345                 && !(c == '.' || c == '_' || c == '-'))
346                 return idx;
347         }
348         return str.length;
349     }
350 
351 
352     /***************************************************************************
353 
354         This function is useful for debug, however it is not intended to be
355         used in production code, as it allocates.
356 
357         Returns:
358             A newly-allocated string suitable for printing.
359 
360     ***************************************************************************/
361 
362     public istring toString ()
363     {
364         auto pi = this.plugin_instance.length ? "-" : null;
365         auto ti = this.type_instance.length ? "-" : null;
366 
367         return format("{}/{}{}{}/{}{}{}", this.host,
368                       this.plugin, pi, this.plugin_instance,
369                       this.type, ti, this.type_instance);
370     }
371 }