a2c9a31119d17d4c5f863def1178799915bb7198
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27
28 local io    = require "io"
29 local os    = require "os"
30 local posix = require "posix"
31 local table = require "table"
32
33 local luci  = {}
34 luci.util   = require "luci.util"
35 luci.fs     = require "luci.fs"
36 luci.ip     = require "luci.ip"
37
38 local tonumber, ipairs, pairs, pcall = tonumber, ipairs, pairs, pcall
39
40
41 --- LuCI Linux and POSIX system utilities.
42 module "luci.sys"
43
44 --- Execute a given shell command and return the error code
45 -- @class               function
46 -- @name                call
47 -- @param               ...             Command to call
48 -- @return              Error code of the command
49 function call(...)
50         return os.execute(...) / 256
51 end
52
53 --- Execute a given shell command and capture its standard output
54 -- @class               function
55 -- @name                exec
56 -- @param command       Command to call
57 -- @return                      String containg the return the output of the command
58 exec = luci.util.exec
59
60 --- Invoke the luci-flash executable to write an image to the flash memory.
61 -- @param image         Local path or URL to image file
62 -- @param kpattern      Pattern of files to keep over flash process
63 -- @return                      Return value of os.execute()
64 function flash(image, kpattern)
65         local cmd = "luci-flash "
66         if kpattern then
67                 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
68         end
69         cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
70
71         return os.execute(cmd)
72 end
73
74 --- Retrieve information about currently mounted file systems.
75 -- @return      Table containing mount information
76 function mounts()
77         local data = {}
78         local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
79         local ps = luci.util.execi("df")
80
81         if not ps then
82                 return
83         else
84                 ps()
85         end
86
87         for line in ps do
88                 local row = {}
89
90                 local j = 1
91                 for value in line:gmatch("[^%s]+") do
92                         row[k[j]] = value
93                         j = j + 1
94                 end
95
96                 if row[k[1]] then
97
98                         -- this is a rather ugly workaround to cope with wrapped lines in
99                         -- the df output:
100                         --
101                         --      /dev/scsi/host0/bus0/target0/lun0/part3
102                         --                   114382024  93566472  15005244  86% /mnt/usb
103                         --
104
105                         if not row[k[2]] then
106                                 j = 2
107                                 line = ps()
108                                 for value in line:gmatch("[^%s]+") do
109                                         row[k[j]] = value
110                                         j = j + 1
111                                 end
112                         end
113
114                         table.insert(data, row)
115                 end
116         end
117
118         return data
119 end
120
121 --- Retrieve environment variables. If no variable is given then a table
122 -- containing the whole environment is returned otherwise this function returns
123 -- the corresponding string value for the given name or nil if no such variable
124 -- exists.
125 -- @class               function
126 -- @name                getenv
127 -- @param var   Name of the environment variable to retrieve (optional)
128 -- @return              String containg the value of the specified variable
129 -- @return              Table containing all variables if no variable name is given
130 getenv = posix.getenv
131
132 --- Determine the current hostname.
133 -- @return              String containing the system hostname
134 function hostname()
135         return posix.uname("%n")
136 end
137
138 --- Returns the contents of a documented referred by an URL.
139 -- @param url    The URL to retrieve
140 -- @param stream Return a stream instead of a buffer
141 -- @param target Directly write to target file name
142 -- @return              String containing the contents of given the URL
143 function httpget(url, stream, target)
144         if not target then
145                 local source = stream and io.popen or luci.util.exec
146                 return source("wget -qO- '"..url:gsub("'", "").."'")
147         else
148                 return os.execute("wget -qO '%s' '%s'" %
149                         {target:gsub("'", ""), url:gsub("'", "")})
150         end
151 end
152
153 --- Returns the system load average values.
154 -- @return      String containing the average load value 1 minute ago
155 -- @return      String containing the average load value 5 minutes ago
156 -- @return      String containing the average load value 15 minutes ago
157 -- @return      String containing the active and total number of processes
158 -- @return      String containing the last used pid
159 function loadavg()
160         local loadavg = io.lines("/proc/loadavg")()
161         return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
162 end
163
164 --- Initiate a system reboot.
165 -- @return      Return value of os.execute()
166 function reboot()
167         return os.execute("reboot >/dev/null 2>&1")
168 end
169
170 --- Returns the system type, cpu name and installed physical memory.
171 -- @return      String containing the system or platform identifier
172 -- @return      String containing hardware model information
173 -- @return      String containing the total memory amount in kB
174 -- @return      String containing the memory used for caching in kB
175 -- @return      String containing the memory used for buffering in kB
176 -- @return      String containing the free memory amount in kB
177 function sysinfo()
178         local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
179         local meminfo = luci.fs.readfile("/proc/meminfo")
180
181         local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
182         local model = ""
183         local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
184         local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
185         local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
186         local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
187
188         if not system then
189                 system = posix.uname("%m")
190                 model = cpuinfo:match("model name.-:%s*([^\n]+)")
191                 if not model then
192                         model = cpuinfo:match("Processor.-:%s*([^\n]+)")
193                 end
194         else
195                 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
196         end
197
198         return system, model, memtotal, memcached, membuffers, memfree
199 end
200
201 --- Retrieves the output of the "logread" command.
202 -- @return      String containing the current log buffer
203 function syslog()
204         return luci.util.exec("logread")
205 end
206
207 --- Retrieves the output of the "dmesg" command.
208 -- @return      String containing the current log buffer
209 function dmesg()
210         return luci.util.exec("dmesg")
211 end
212
213 --- Generates a random id with specified length.
214 -- @param bytes Number of bytes for the unique id
215 -- @return              String containing hex encoded id
216 function uniqueid(bytes)
217         local fp    = io.open("/dev/urandom")
218         local chunk = { fp:read(bytes):byte(1, bytes) }
219         fp:close()
220
221         local hex = ""
222
223         local pattern = "%02X"
224         for i, byte in ipairs(chunk) do
225                 hex = hex .. pattern:format(byte)
226         end
227
228         return hex
229 end
230
231 --- Returns the current system uptime stats.
232 -- @return      String containing total uptime in seconds
233 -- @return      String containing idle time in seconds
234 function uptime()
235         local loadavg = io.lines("/proc/uptime")()
236         return loadavg:match("^(.-) (.-)$")
237 end
238
239 --- LuCI system utilities / POSIX user group related functions.
240 -- @class       module
241 -- @name        luci.sys.group
242 group = {}
243
244 --- Returns information about a POSIX user group.
245 -- @class function
246 -- @name                getgroup
247 -- @param group Group ID or name of a system user group
248 -- @return      Table with information about the requested group
249 group.getgroup = posix.getgroup
250
251
252 --- LuCI system utilities / network related functions.
253 -- @class       module
254 -- @name        luci.sys.net
255 net = {}
256
257 --- Returns the current arp-table entries as two-dimensional table.
258 -- @return      Table of table containing the current arp entries.
259 --                      The following fields are defined for arp entry objects:
260 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
261 function net.arptable()
262         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
263 end
264
265 --- Returns conntrack information
266 -- @return      Table with the currently tracked IP connections
267 function net.conntrack()
268         local connt = {}
269         if luci.fs.access("/proc/net/nf_conntrack", "r") then
270                 for line in io.lines("/proc/net/nf_conntrack") do
271                         line = line:match "^(.-( [^ =]+=).-)%2"
272                         local entry, flags = _parse_mixed_record(line, " +")
273                         entry.layer3 = flags[1]
274                         entry.layer4 = flags[3]
275                         for i=1, #entry do
276                                 entry[i] = nil
277                         end
278
279                         connt[#connt+1] = entry
280                 end
281         elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
282                 for line in io.lines("/proc/net/ip_conntrack") do
283                         line = line:match "^(.-( [^ =]+=).-)%2"
284                         local entry, flags = _parse_mixed_record(line, " +")
285                         entry.layer3 = "ipv4"
286                         entry.layer4 = flags[1]
287                         for i=1, #entry do
288                                 entry[i] = nil
289                         end
290
291                         connt[#connt+1] = entry
292                 end
293         else
294                 return nil
295         end
296         return connt
297 end
298
299 --- Determine the current IPv4 default route. If multiple default routes exist,
300 -- return the one with the lowest metric.
301 -- @return      Table with the properties of the current default route.
302 --                      The following fields are defined:
303 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
304 --                        "flags", "device" }
305 function net.defaultroute()
306         local route = nil
307         for _, r in pairs(net.routes()) do
308                 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
309                         route = r
310                 end
311         end
312         return route
313 end
314
315 --- Determine the current IPv6 default route. If multiple default routes exist,
316 -- return the one with the lowest metric.
317 -- @return      Table with the properties of the current default route.
318 --                      The following fields are defined:
319 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
320 --                        "flags", "device" }
321 function net.defaultroute6()
322         local route   = nil
323         local routes6 = net.routes6()
324         if routes6 then
325                 for _, r in pairs(routes6) do
326                         if r.dest:prefix() == 0 and
327                            (not route or route.metric > r.metric)
328                         then
329                                 route = r
330                         end
331                 end
332         end
333         return route
334 end
335
336 --- Determine the names of available network interfaces.
337 -- @return      Table containing all current interface names
338 function net.devices()
339         local devices = {}
340         for line in io.lines("/proc/net/dev") do
341                 table.insert(devices, line:match(" *(.-):"))
342         end
343         return devices
344 end
345
346
347 --- Return information about available network interfaces.
348 -- @return      Table containing all current interface names and their information
349 function net.deviceinfo()
350         local devices = {}
351         for line in io.lines("/proc/net/dev") do
352                 local name, data = line:match("^ *(.-): *(.*)$")
353                 if name and data then
354                         devices[name] = luci.util.split(data, " +", nil, true)
355                 end
356         end
357         return devices
358 end
359
360
361 -- Determine the MAC address belonging to the given IP address.
362 -- @param ip    IPv4 address
363 -- @return              String containing the MAC address or nil if it cannot be found
364 function net.ip4mac(ip)
365         local mac = nil
366
367         for i, l in ipairs(net.arptable()) do
368                 if l["IP address"] == ip then
369                         mac = l["HW address"]
370                 end
371         end
372
373         return mac
374 end
375
376 --- Returns the current kernel routing table entries.
377 -- @return      Table of tables with properties of the corresponding routes.
378 --                      The following fields are defined for route entry tables:
379 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
380 --                        "flags", "device" }
381 function net.routes()
382         local routes = { }
383
384         for line in io.lines("/proc/net/route") do
385
386                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
387                           dst_mask, mtu, win, irtt = line:match(
388                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
389                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
390                 )
391
392                 if dev then
393                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
394                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
395                         dst_ip   = luci.ip.Hex(
396                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
397                         )
398
399                         routes[#routes+1] = {
400                                 dest     = dst_ip,
401                                 gateway  = gateway,
402                                 metric   = tonumber(metric),
403                                 refcount = tonumber(refcnt),
404                                 usecount = tonumber(usecnt),
405                                 mtu      = tonumber(mtu),
406                                 window   = tonumber(window),
407                                 irtt     = tonumber(irtt),
408                                 flags    = tonumber(flags, 16),
409                                 device   = dev
410                         }
411                 end
412         end
413
414         return routes
415 end
416
417 --- Returns the current ipv6 kernel routing table entries.
418 -- @return      Table of tables with properties of the corresponding routes.
419 --                      The following fields are defined for route entry tables:
420 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
421 --                        "flags", "device" }
422 function net.routes6()
423         if luci.fs.access("/proc/net/ipv6_route", "r") then
424                 local routes = { }
425
426                 for line in io.lines("/proc/net/ipv6_route") do
427
428                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
429                                   metric, refcnt, usecnt, flags, dev = line:match(
430                                 "([a-f0-9]+) ([a-f0-9]+) " ..
431                                 "([a-f0-9]+) ([a-f0-9]+) " ..
432                                 "([a-f0-9]+) ([a-f0-9]+) " ..
433                                 "([a-f0-9]+) ([a-f0-9]+) " ..
434                                 "([a-f0-9]+) +([^%s]+)"
435                         )
436
437                         src_ip = luci.ip.Hex(
438                                 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
439                         )
440
441                         dst_ip = luci.ip.Hex(
442                                 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
443                         )
444
445                         nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
446
447                         routes[#routes+1] = {
448                                 source   = src_ip,
449                                 dest     = dst_ip,
450                                 nexthop  = nexthop,
451                                 metric   = tonumber(metric, 16),
452                                 refcount = tonumber(refcnt, 16),
453                                 usecount = tonumber(usecnt, 16),
454                                 flags    = tonumber(flags, 16),
455                                 device   = dev
456                         }
457                 end
458
459                 return routes
460         end
461 end
462
463 --- Tests whether the given host responds to ping probes.
464 -- @param host  String containing a hostname or IPv4 address
465 -- @return              Number containing 0 on success and >= 1 on error
466 function net.pingtest(host)
467         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
468 end
469
470
471 --- LuCI system utilities / process related functions.
472 -- @class       module
473 -- @name        luci.sys.process
474 process = {}
475
476 --- Get the current process id.
477 -- @class function
478 -- @name  process.info
479 -- @return      Number containing the current pid
480 process.info = posix.getpid
481
482 --- Retrieve information about currently running processes.
483 -- @return      Table containing process information
484 function process.list()
485         local data = {}
486         local k
487         local ps = luci.util.execi("top -bn1")
488
489         if not ps then
490                 return
491         end
492
493         while true do
494                 local line = ps()
495                 if not line then
496                         return
497                 end
498
499                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
500                 if k[1] == "PID" then
501                         break
502                 end
503         end
504
505         for line in ps do
506                 local row = {}
507
508                 line = luci.util.trim(line)
509                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
510                         row[k[i]] = value
511                 end
512
513                 local pid = tonumber(row[k[1]])
514                 if pid then
515                         data[pid] = row
516                 end
517         end
518
519         return data
520 end
521
522 --- Set the gid of a process identified by given pid.
523 -- @param pid   Number containing the process id
524 -- @param gid   Number containing the Unix group id
525 -- @return              Boolean indicating successful operation
526 -- @return              String containing the error message if failed
527 -- @return              Number containing the error code if failed
528 function process.setgroup(pid, gid)
529         return posix.setpid("g", pid, gid)
530 end
531
532 --- Set the uid of a process identified by given pid.
533 -- @param pid   Number containing the process id
534 -- @param uid   Number containing the Unix user id
535 -- @return              Boolean indicating successful operation
536 -- @return              String containing the error message if failed
537 -- @return              Number containing the error code if failed
538 function process.setuser(pid, uid)
539         return posix.setpid("u", pid, uid)
540 end
541
542 --- Send a signal to a process identified by given pid.
543 -- @class function
544 -- @name  process.signal
545 -- @param pid   Number containing the process id
546 -- @param sig   Signal to send (default: 15 [SIGTERM])
547 -- @return              Boolean indicating successful operation
548 -- @return              Number containing the error code if failed
549 process.signal = posix.kill
550
551
552 --- LuCI system utilities / user related functions.
553 -- @class       module
554 -- @name        luci.sys.user
555 user = {}
556
557 --- Retrieve user informations for given uid.
558 -- @class               function
559 -- @name                getuser
560 -- @param uid   Number containing the Unix user id
561 -- @return              Table containing the following fields:
562 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
563 user.getuser = posix.getpasswd
564
565 --- Test whether given string matches the password of a given system user.
566 -- @param username      String containing the Unix user name
567 -- @param password      String containing the password to compare
568 -- @return                      Boolean indicating wheather the passwords are equal
569 function user.checkpasswd(username, password)
570         local account = user.getuser(username)
571
572         if account then
573                 local pwd = account.passwd
574                 local shadowpw
575                 if #pwd == 1 then
576                         if luci.fs.stat("/etc/shadow") then
577                                 if not pcall(function()
578                                         for l in io.lines("/etc/shadow") do
579                                                 shadowpw = l:match("^%s:([^:]+)" % username)
580                                                 if shadowpw then
581                                                         pwd = shadowpw
582                                                         break
583                                                 end
584                                         end
585                                 end) then
586                                         return nil, "Unable to access shadow-file"
587                                 end
588                         end
589
590                         if pwd == "!" then
591                                 return true
592                         end
593                 end
594
595                 if pwd and #pwd > 0 and password and #password > 0 then
596                         return (pwd == posix.crypt(password, pwd))
597                 end
598         end
599
600         return false
601 end
602
603 --- Change the password of given user.
604 -- @param username      String containing the Unix user name
605 -- @param password      String containing the password to compare
606 -- @return                      Number containing 0 on success and >= 1 on error
607 function user.setpasswd(username, password)
608         if password then
609                 password = password:gsub("'", "")
610         end
611
612         if username then
613                 username = username:gsub("'", "")
614         end
615
616         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
617         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
618         return os.execute(cmd)
619 end
620
621
622 --- LuCI system utilities / wifi related functions.
623 -- @class       module
624 -- @name        luci.sys.wifi
625 wifi = {}
626
627 --- Get iwconfig output for all wireless devices.
628 -- @return      Table of tables containing the iwconfing output for each wifi device
629 function wifi.getiwconfig()
630         local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
631         local iwc = {}
632
633         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
634                 local k = l:match("^(.-) ")
635                 l = l:gsub("^(.-) +", "", 1)
636                 if k then
637                         local entry, flags = _parse_mixed_record(l)
638                         if entry then
639                                 entry.flags = flags
640                         end
641                         iwc[k] = entry
642                 end
643         end
644
645         return iwc
646 end
647
648 --- Get iwlist scan output from all wireless devices.
649 -- @return      Table of tables contaiing all scan results
650 function wifi.iwscan(iface)
651         local siface = iface or ""
652         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
653         local iws = {}
654
655         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
656                 local k = l:match("^(.-) ")
657                 l = l:gsub("^[^\n]+", "", 1)
658                 l = luci.util.trim(l)
659                 if k then
660                         iws[k] = {}
661                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
662                                 c = c:gsub("^(.-)- ", "", 1)
663                                 c = luci.util.split(c, "\n", 7)
664                                 c = table.concat(c, "\n", 1)
665                                 local entry, flags = _parse_mixed_record(c)
666                                 if entry then
667                                         entry.flags = flags
668                                 end
669                                 table.insert(iws[k], entry)
670                         end
671                 end
672         end
673
674         return iface and (iws[iface] or {}) or iws
675 end
676
677
678 --- LuCI system utilities / init related functions.
679 -- @class       module
680 -- @name        luci.sys.init
681 init = {}
682 init.dir = "/etc/init.d/"
683
684 --- Get the names of all installed init scripts
685 -- @return      Table containing the names of all inistalled init scripts
686 function init.names()
687         local names = { }
688         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
689                 names[#names+1] = luci.fs.basename(name)
690         end
691         return names
692 end
693
694 --- Test whether the given init script is enabled
695 -- @param name  Name of the init script
696 -- @return              Boolean indicating whether init is enabled
697 function init.enabled(name)
698         if luci.fs.access(init.dir..name) then
699                 return ( call(init.dir..name.." enabled") == 0 )
700         end
701         return false
702 end
703
704 --- Get the index of he given init script
705 -- @param name  Name of the init script
706 -- @return              Numeric index value
707 function init.index(name)
708         if luci.fs.access(init.dir..name) then
709                 return call("source "..init.dir..name.."; exit $START")
710         end
711 end
712
713 --- Enable the given init script
714 -- @param name  Name of the init script
715 -- @return              Boolean indicating success
716 function init.enable(name)
717         if luci.fs.access(init.dir..name) then
718                 return ( call(init.dir..name.." enable") == 1 )
719         end
720 end
721
722 --- Disable the given init script
723 -- @param name  Name of the init script
724 -- @return              Boolean indicating success
725 function init.disable(name)
726         if luci.fs.access(init.dir..name) then
727                 return ( call(init.dir..name.." disable") == 0 )
728         end
729 end
730
731
732 -- Internal functions
733
734 function _parse_delimited_table(iter, delimiter)
735         delimiter = delimiter or "%s+"
736
737         local data  = {}
738         local trim  = luci.util.trim
739         local split = luci.util.split
740
741         local keys = split(trim(iter()), delimiter, nil, true)
742         for i, j in pairs(keys) do
743                 keys[i] = trim(keys[i])
744         end
745
746         for line in iter do
747                 local row = {}
748                 line = trim(line)
749                 if #line > 0 then
750                         for i, j in pairs(split(line, delimiter, nil, true)) do
751                                 if keys[i] then
752                                         row[keys[i]] = j
753                                 end
754                         end
755                 end
756                 table.insert(data, row)
757         end
758
759         return data
760 end
761
762 function _parse_mixed_record(cnt, delimiter)
763         delimiter = delimiter or "  "
764         local data = {}
765         local flags = {}
766
767         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
768                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
769                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
770
771                         if k then
772                                 if x == "" then
773                                         table.insert(flags, k)
774                                 else
775                                         data[k] = v
776                                 end
777                         end
778                 end
779         end
780
781         return data, flags
782 end