1 /++
2     Functionality generic enough to be used in several places.
3  +/
4 module lu.common;
5 
6 private:
7 
8 import std.typecons : Flag, No, Yes;
9 
10 public:
11 
12 @safe:
13 
14 
15 // Next
16 /++
17     Enum of flags carrying the meaning of "what to do next".
18  +/
19 enum Next
20 {
21     /++
22         Keep doing whatever is being done, alternatively continue on to the next step.
23      +/
24     continue_,
25 
26     /++
27         Halt what's being done and give it another attempt.
28      +/
29     retry,
30 
31     /++
32         Exit or return with a positive return value.
33      +/
34     returnSuccess,
35 
36     /++
37         Exit or abort with a negative return value.
38      +/
39     returnFailure,
40 
41     /++
42         Fatally abort.
43      +/
44     crash,
45 }
46 
47 
48 // ReturnValueException
49 /++
50     Exception, to be thrown when an executed command returns an error value.
51 
52     It is a normal [object.Exception|Exception] but with an attached command
53     and return value.
54  +/
55 final class ReturnValueException : Exception
56 {
57     /// The command run.
58     string command;
59 
60     /// The value returned.
61     int retval;
62 
63     /// Create a new [ReturnValueException], without attaching anything.
64     this(const string message,
65         const string file = __FILE__,
66         const size_t line = __LINE__,
67         Throwable nextInChain = null) pure nothrow @nogc @safe
68     {
69         super(message, file, line, nextInChain);
70     }
71 
72     /// Create a new [ReturnValueException], attaching a command.
73     this(const string message,
74         const string command,
75         const string file = __FILE__,
76         const size_t line = __LINE__,
77         Throwable nextInChain = null) pure nothrow @nogc @safe
78     {
79         this.command = command;
80         super(message, file, line, nextInChain);
81     }
82 
83     /// Create a new [ReturnValueException], attaching a command and a returned value.
84     this(const string message,
85         const string command,
86         const int retval,
87         const string file = __FILE__,
88         const size_t line = __LINE__,
89         Throwable nextInChain = null) pure nothrow @nogc @safe
90     {
91         this.command = command;
92         this.retval = retval;
93         super(message, file, line, nextInChain);
94     }
95 }
96 
97 
98 // FileExistsException
99 /++
100     Exception, to be thrown when attempting to create a file or directory and
101     finding that one already exists with the same name.
102 
103     It is a normal [object.Exception|Exception] but with an attached filename string.
104  +/
105 final class FileExistsException : Exception
106 {
107     /// The name of the file.
108     string filename;
109 
110     /// Create a new [FileExistsException], without attaching a filename.
111     this(const string message,
112         const string file = __FILE__,
113         const size_t line = __LINE__,
114         Throwable nextInChain = null) pure nothrow @nogc @safe
115     {
116         super(message, file, line, nextInChain);
117     }
118 
119     /// Create a new [FileExistsException], attaching a filename.
120     this(const string message,
121         const string filename,
122         const string file = __FILE__,
123         const size_t line = __LINE__,
124         Throwable nextInChain = null) pure nothrow @nogc @safe
125     {
126         this.filename = filename;
127         super(message, file, line, nextInChain);
128     }
129 }
130 
131 
132 // FileTypeMismatchException
133 /++
134     Exception, to be thrown when attempting to access a file or directory and
135     finding that something with the that name exists, but is of an unexpected type.
136 
137     It is a normal [object.Exception|Exception] but with an embedded filename
138     string, and an uint representing the existing file's type (file, directory,
139     symlink, ...).
140  +/
141 final class FileTypeMismatchException : Exception
142 {
143     /// The filename of the non-FIFO.
144     string filename;
145 
146     /// File attributes.
147     ushort attrs;
148 
149     /// Create a new [FileTypeMismatchException], without embedding a filename.
150     this(const string message,
151         const string file = __FILE__,
152         const size_t line = __LINE__,
153         Throwable nextInChain = null) pure nothrow @nogc @safe
154     {
155         super(message, file, line, nextInChain);
156     }
157 
158     /// Create a new [FileTypeMismatchException], embedding a filename.
159     this(const string message,
160         const string filename,
161         const ushort attrs,
162         const string file = __FILE__,
163         const size_t line = __LINE__,
164         Throwable nextInChain = null) pure nothrow @nogc @safe
165     {
166         this.filename = filename;
167         this.attrs = attrs;
168         super(message, file, line, nextInChain);
169     }
170 }
171 
172 
173 // sharedDomains
174 /++
175     Calculates how many dot-separated suffixes two strings share.
176 
177     This is useful to see to what extent two addresses are similar.
178 
179     Example:
180     ---
181     int numDomains = sharedDomains("irc.freenode.net", "leguin.freenode.net");
182     assert(numDomains == 2);  // freenode.net
183 
184     int numDomains2 = sharedDomains("Portlane2.EU.GameSurge.net", "services.gamesurge.net", No.caseSensitive);
185     assert(numDomains2 == 2);  // gamesurge.net
186     ---
187 
188     Params:
189         one = First domain string.
190         other = Second domain string.
191         caseSensitive = Whether or not comparison should be done on a
192             case-sensitive basis.
193 
194     Returns:
195         The number of domains the two strings share.
196 
197     TODO:
198         Support partial globs.
199  +/
200 auto sharedDomains(
201     const string one,
202     const string other,
203     const Flag!"caseSensitive" caseSensitive = Yes.caseSensitive) pure @nogc nothrow
204 {
205     if (!one.length || !other.length) return 0;
206 
207     static uint numDomains(const char[] one, const char[] other, const bool caseSensitive)
208     {
209         uint dots;
210         double doubleDots;
211 
212         // If both strings are the same, act as if there's an extra dot.
213         // That gives (.)rizon.net and (.)rizon.net two suffixes.
214 
215         if (caseSensitive)
216         {
217             if (one == other) ++dots;
218         }
219         else
220         {
221             import std.algorithm.comparison : equal;
222             import std.uni : asLowerCase;
223             if (one.asLowerCase.equal(other.asLowerCase)) ++dots;
224         }
225 
226         foreach (immutable i; 0..one.length)
227         {
228             immutable c1 = one[$-i-1];
229 
230             if (i == other.length)
231             {
232                 if (c1 == '.') ++dots;
233                 break;
234             }
235 
236             immutable c2 = other[$-i-1];
237 
238             if (caseSensitive)
239             {
240                 if (c1 != c2) break;
241             }
242             else
243             {
244                 import std.ascii : toLower;
245                 if (c1.toLower != c2.toLower) break;
246             }
247 
248             if (c1 == '.')
249             {
250                 if (!doubleDots)
251                 {
252                     ++dots;
253                     doubleDots = true;
254                 }
255             }
256             else
257             {
258                 doubleDots = false;
259             }
260         }
261 
262         return dots;
263     }
264 
265     return (one.length > other.length) ?
266         numDomains(one, other, cast(bool)caseSensitive) :
267         numDomains(other, one, cast(bool)caseSensitive);
268 }
269 
270 ///
271 unittest
272 {
273     import std.conv : text;
274 
275     immutable n1 = sharedDomains("irc.freenode.net", "help.freenode.net");
276     assert((n1 == 2), n1.text);
277 
278     immutable n2 = sharedDomains("irc.rizon.net", "services.rizon.net");
279     assert((n2 == 2), n2.text);
280 
281     immutable n3 = sharedDomains("www.google.com", "www.yahoo.com");
282     assert((n3 == 1), n3.text);
283 
284     immutable n4 = sharedDomains("www.google.se", "www.google.co.uk");
285     assert((n4 == 0), n4.text);
286 
287     immutable n5 = sharedDomains("", string.init);
288     assert((n5 == 0), n5.text);
289 
290     immutable n6 = sharedDomains("irc.rizon.net", "rizon.net");
291     assert((n6 == 2), n6.text);
292 
293     immutable n7 = sharedDomains("rizon.net", "rizon.net");
294     assert((n7 == 2), n7.text);
295 
296     immutable n8 = sharedDomains("net", "net");
297     assert((n8 == 1), n8.text);
298 
299     immutable n9 = sharedDomains("forum.dlang.org", "...");
300     assert((n9 == 0), n8.text);
301 
302     immutable n10 = sharedDomains("blahrizon.net", "rizon.net");
303     assert((n10 == 1), n10.text);
304 
305     immutable n11 = sharedDomains("rizon.net", "blahrizon.net");
306     assert((n11 == 1), n11.text);
307 
308     immutable n12 = sharedDomains("rizon.net", "irc.rizon.net");
309     assert((n12 == 2), n12.text);
310 
311     immutable n13 = sharedDomains("irc.gamesurge.net", "Stuff.GameSurge.net", No.caseSensitive);
312     assert((n13 == 2), n13.text);
313 
314     immutable n14 = sharedDomains("irc.freenode.net", "irc.FREENODE.net", No.caseSensitive);
315     assert((n14 == 3), n14.text);
316 
317     immutable n15 = sharedDomains("irc.SpotChat.org", "irc.FREENODE.net", No.caseSensitive);
318     assert((n15 == 0), n15.text);
319 }