1 /**
2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module dsnapshot.stats;
7 
8 import logger = std.experimental.logger;
9 
10 import dsnapshot.types : Path;
11 
12 version (unittest) {
13     import unit_threaded.assertions;
14 }
15 
16 @safe:
17 
18 /// Stats for a dev+inode as it is in a fakeroot environment.
19 struct FakerootStat {
20     static struct Node {
21         // ID of device containing file
22         ulong dev;
23         // Inode number
24         ulong inode;
25 
26         size_t toHash() @safe pure nothrow const @nogc scope {
27             auto a = dev.hashOf();
28             return inode.hashOf(a); // mixing two hash values
29         }
30 
31         bool opEquals()(auto ref const S s) const {
32             return s.dev == dev && s.inode == inode;
33         }
34     }
35 
36     Node node;
37     alias node this;
38 
39     // File type and mode
40     ulong mode;
41     // User ID of owner
42     ulong uid;
43     // Group ID of owner
44     ulong gid;
45     // Device ID (if special file)
46     ulong rdev;
47 
48     import std.range : isOutputRange;
49 
50     string toString() @safe pure const {
51         import std.array : appender;
52 
53         auto buf = appender!string;
54         toString(buf);
55         return buf.data;
56     }
57 
58     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
59         import std.format : formattedWrite;
60         import std.range : put;
61 
62         put(w, typeof(this).stringof);
63         put(w, "(");
64         formattedWrite(w, "dev=%s", node.dev);
65         formattedWrite(w, ",inode=%s", node.inode);
66         formattedWrite(w, ",hash=%s", node.toHash);
67         formattedWrite(w, ",mode=%s", mode);
68         formattedWrite(w, ",uid=%s", uid);
69         formattedWrite(w, ",gid=%s", gid);
70         formattedWrite(w, ",rdev=%s", rdev);
71         put(w, ")");
72     }
73 }
74 
75 /** Parse a fakeroot configuration line.
76  *
77  * Example:
78  * dev=37,ino=15312816,mode=100664,uid=1000,gid=1000,rdev=0
79  */
80 FakerootStat fromFakeroot(const(char)[] s) {
81     import std.format : formattedRead;
82 
83     typeof(return) rval;
84     ulong nlink;
85     s.formattedRead!"dev=%x,ino=%d,mode=%o,uid=%d,gid=%d,nlink=%d,rdev=%d"(rval.dev,
86             rval.inode, rval.mode, rval.uid, rval.gid, nlink, rval.rdev);
87     return rval;
88 }
89 
90 @("shall convert a fakeroot line to FakerootStat")
91 unittest {
92     auto res = fromFakeroot("dev=37,ino=15312816,mode=100664,uid=1000,gid=1000,nlink=1,rdev=0");
93     res.dev.shouldEqual(55);
94     res.inode.shouldEqual(15312816);
95     res.mode.shouldEqual(33204);
96     res.uid.shouldEqual(1000);
97     res.gid.shouldEqual(1000);
98     res.rdev.shouldEqual(0);
99 }
100 
101 struct FakerootDb {
102     import std.typecons : Nullable;
103 
104     alias Set = FakerootStat[FakerootStat.Node];
105     Set db;
106 
107     void put(FakerootStat v) {
108         db[v.node] = v;
109     }
110 
111     Nullable!FakerootStat get(FakerootStat.Node node) {
112         typeof(return) rval;
113         if (auto v = node in db) {
114             rval = *v;
115         }
116         return rval;
117     }
118 
119     import std.range : isOutputRange;
120 
121     string toString() @safe pure const {
122         import std.array : appender;
123 
124         auto buf = appender!string;
125         toString(buf);
126         return buf.data;
127     }
128 
129     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
130         import std.range : put;
131 
132         put(w, typeof(this).stringof);
133         put(w, "(");
134 
135         foreach (const a; db.byValue) {
136             a.toString(w);
137             put(w, ",");
138         }
139 
140         put(w, ")");
141     }
142 }
143 
144 /// Stats for a path.
145 struct PathStat {
146     /// Path relative to the destination.
147     string path;
148 
149     /// File type and mode
150     ulong mode;
151     /// User ID of owner
152     ulong uid;
153     /// Group ID of owner
154     ulong gid;
155     /// Device ID (if special file)
156     ulong rdev;
157 
158     import std.range : isOutputRange;
159 
160     string toString() @safe pure const {
161         import std.array : appender;
162 
163         auto buf = appender!string;
164         toString(buf);
165         return buf.data;
166     }
167 
168     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
169         import std.format : formattedWrite;
170         import std.range : put;
171 
172         formattedWrite(w, "mode=%o", mode);
173         formattedWrite(w, ",uid=%s", uid);
174         formattedWrite(w, ",gid=%s", gid);
175         formattedWrite(w, ",rdev=%s", rdev);
176         formattedWrite(w, ",path=%s", path);
177     }
178 }
179 
180 @("shall convert PathStat to a string")
181 unittest {
182     import std.conv : octal;
183 
184     PathStat("foo/bar", octal!100664, 1000, 1001, 0).toString.shouldEqual(
185             "mode=100664,uid=1000,gid=1001,rdev=0,path=foo/bar");
186 }
187 
188 PathStat fromPathStat(const(char)[] s) {
189     import std.format : formattedRead;
190 
191     typeof(return) rval;
192     s.formattedRead!"mode=%o,uid=%d,gid=%d,rdev=%d,path=%s"(rval.mode,
193             rval.uid, rval.gid, rval.rdev, rval.path);
194     return rval;
195 }
196 
197 @("shall convert from string to pathstat")
198 unittest {
199     import std.conv : octal;
200 
201     auto p = fromPathStat("mode=100664,uid=1000,gid=1001,rdev=0,path=foo/bar");
202     p.shouldEqual(PathStat("foo/bar", octal!100664, 1000, 1001, 0));
203 }
204 
205 PathStat[] fromFakeroot(ref FakerootDb db, const string root, const string relRoot) @trusted {
206     import std.algorithm : filter;
207     import std.array : array;
208     import std.file : dirEntries, SpanMode;
209     import std.path : relativePath;
210 
211     PathStat[string] rval;
212 
213     foreach (f; dirEntries(root, SpanMode.depth).filter!(a => a.name !in rval)) {
214         const st = f.statBuf;
215         auto stdb = db.get(FakerootStat.Node(st.st_dev, st.st_ino));
216         if (!stdb.isNull)
217             rval[f.name] = PathStat(relativePath(f.name, relRoot),
218                     stdb.get.mode, stdb.get.uid, stdb.get.gid, stdb.get.rdev);
219     }
220 
221     return rval.byValue.array;
222 }
223 
224 FakerootDb fromFakerootEnv(const Path p) @trusted {
225     import std.stdio : File;
226 
227     FakerootDb fkdb;
228     foreach (const l; File(p.toString).byLine)
229         fkdb.put(fromFakeroot(l));
230     return fkdb;
231 }