Passed
Push — master ( c1e68f...f0bd78 )
by Sam
01:33 queued 10s
created

AlignedBuilder::compareResourceRecords()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 13
c 1
b 0
f 0
nc 20
nop 2
dl 0
loc 31
ccs 14
cts 14
cp 1
crap 9
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Badcow DNS Library.
7
 *
8
 * (c) Samuel Williams <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Badcow\DNS;
15
16
use Badcow\DNS\Parser\Tokens;
17
use Badcow\DNS\Rdata\A;
18
use Badcow\DNS\Rdata\AAAA;
19
use Badcow\DNS\Rdata\CNAME;
20
use Badcow\DNS\Rdata\DNAME;
21
use Badcow\DNS\Rdata\HINFO;
22
use Badcow\DNS\Rdata\LOC;
23
use Badcow\DNS\Rdata\MX;
24
use Badcow\DNS\Rdata\NS;
25
use Badcow\DNS\Rdata\NSEC3;
26
use Badcow\DNS\Rdata\NSEC3PARAM;
27
use Badcow\DNS\Rdata\PTR;
28
use Badcow\DNS\Rdata\RdataInterface;
29
use Badcow\DNS\Rdata\RRSIG;
30
use Badcow\DNS\Rdata\SOA;
31
use Badcow\DNS\Rdata\SRV;
32
use Badcow\DNS\Rdata\TXT;
33
34
class AlignedBuilder
35
{
36
    /**
37
     * The order in which Resource Records should appear in a zone.
38
     *
39
     * @var array
40
     */
41
    private static $order = [
42
        SOA::TYPE,
43
        NS::TYPE,
44
        A::TYPE,
45
        AAAA::TYPE,
46
        CNAME::TYPE,
47
        DNAME::TYPE,
48
        MX::TYPE,
49
        LOC::TYPE,
50
        HINFO::TYPE,
51
        TXT::TYPE,
52
        PTR::TYPE,
53
        SRV::TYPE,
54
        NSEC3::TYPE,
55
        NSEC3PARAM::TYPE,
56
        RRSIG::TYPE,
57
    ];
58
59
    /**
60
     * Build an aligned BIND zone file.
61
     *
62
     * @param Zone $zone
63
     *
64
     * @return string
65
     */
66 3
    public static function build(Zone $zone): string
67
    {
68 3
        $master = self::generateControlEntries($zone);
69 3
        $resourceRecords = $zone->getResourceRecords();
70 3
        $current = SOA::TYPE;
71 3
        usort($resourceRecords, [__CLASS__, 'compareResourceRecords']);
72
73 3
        list($namePadding, $ttlPadding, $typePadding, $classPadding, $rdataPadding) = self::getPadding($zone);
74
75 3
        foreach ($resourceRecords as $resourceRecord) {
76 3
            $rdata = $resourceRecord->getRdata();
77 3
            if (null == $rdata) {
78 1
                continue;
79
            }
80
81 3
            if ($rdata->getType() !== $current) {
82 3
                $master .= Tokens::LINE_FEED.Tokens::SEMICOLON.Tokens::SPACE.$rdata->getType().' RECORDS'.Tokens::LINE_FEED;
83 3
                $current = $rdata->getType();
84
            }
85
86 3
            $master .= sprintf('%s %s %s %s %s',
87 3
                str_pad((string) $resourceRecord->getName(), $namePadding, Tokens::SPACE, STR_PAD_RIGHT),
88 3
                str_pad((string) $resourceRecord->getTtl(), $ttlPadding, Tokens::SPACE, STR_PAD_RIGHT),
89 3
                str_pad((string) $resourceRecord->getClass(), $classPadding, Tokens::SPACE, STR_PAD_RIGHT),
90 3
                str_pad($rdata->getType(), $typePadding, Tokens::SPACE, STR_PAD_RIGHT),
91 3
                self::generateRdataOutput($rdata, $rdataPadding)
92
            );
93
94 3
            $master .= self::generateComment($resourceRecord);
95 3
            $master .= Tokens::LINE_FEED;
96
        }
97
98 3
        return $master;
99
    }
100
101
    /**
102
     * Returns the control entries as strings.
103
     *
104
     * @param Zone $zone
105
     *
106
     * @return string
107
     */
108 3
    private static function generateControlEntries(Zone $zone): string
109
    {
110 3
        $master = '$ORIGIN '.$zone->getName().Tokens::LINE_FEED;
111 3
        if (null !== $zone->getDefaultTtl()) {
0 ignored issues
show
introduced by
The condition null !== $zone->getDefaultTtl() is always true.
Loading history...
112 3
            $master .= '$TTL '.$zone->getDefaultTtl().Tokens::LINE_FEED;
113
        }
114
115 3
        return $master;
116
    }
117
118
    /**
119
     * Returns a comment string if the comments are not null, returns empty string otherwise.
120
     *
121
     * @param ResourceRecord $resourceRecord
122
     *
123
     * @return string
124
     */
125 3
    private static function generateComment(ResourceRecord $resourceRecord): string
126
    {
127 3
        if (null !== $resourceRecord->getComment()) {
128 3
            return Tokens::SEMICOLON.Tokens::SPACE.$resourceRecord->getComment();
129
        }
130
131 3
        return '';
132
    }
133
134
    /**
135
     * Compares two ResourceRecords to determine which is the higher order. Used with the usort() function.
136
     *
137
     * @param ResourceRecord $a
138
     * @param ResourceRecord $b
139
     *
140
     * @return int
141
     */
142 4
    public static function compareResourceRecords(ResourceRecord $a, ResourceRecord $b): int
143
    {
144 4
        $a_rdata = (null === $a->getRdata()) ? '' : $a->getRdata()->toText();
145 4
        $b_rdata = (null === $b->getRdata()) ? '' : $b->getRdata()->toText();
146
147
        //If the types are the same, do a simple alphabetical comparison.
148 4
        if ($a->getType() === $b->getType()) {
149 4
            return strcmp($a->getName().$a_rdata, $b->getName().$b_rdata);
150
        }
151
152
        //Find the precedence (if any) for the two types.
153 4
        $_a = array_search($a->getType(), self::$order);
154 4
        $_b = array_search($b->getType(), self::$order);
155
156
        //If neither types have defined precedence.
157 4
        if (!is_int($_a) && !is_int($_b)) {
158 1
            return strcmp($a->getType() ?? '', $b->getType() ?? '');
159
        }
160
161
        //If both types have defined precedence.
162 4
        if (is_int($_a) && is_int($_b)) {
163 4
            return $_a - $_b;
164
        }
165
166
        //If only $b has defined precedence.
167 3
        if (false === $_a) {
168 1
            return 1;
169
        }
170
171
        //If only $a has defined precedence.
172 3
        return -1;
173
    }
174
175
    /**
176
     * Composes the RDATA of the Resource Record.
177
     *
178
     * @param RdataInterface $rdata
179
     * @param int            $padding
180
     *
181
     * @return string
182
     */
183 3
    private static function generateRdataOutput(RdataInterface $rdata, int $padding): string
184
    {
185 3
        $rdataFormatters = AlignedRdataFormatters::getRdataFormatters();
186 3
        if (array_key_exists($rdata->getType(), $rdataFormatters)) {
187 3
            return call_user_func($rdataFormatters[$rdata->getType()], $rdata, $padding);
188
        }
189
190 3
        return $rdata->toText();
191
    }
192
193
    /**
194
     * Get the padding required for a zone.
195
     *
196
     * @param Zone $zone
197
     *
198
     * @return int[] Array order: [name, ttl, type, class, rdata]
199
     */
200 3
    private static function getPadding(Zone $zone): array
201
    {
202 3
        $name = $ttl = $type = 0;
203 3
        $class = 2;
204
205
        /** @var ResourceRecord $resourceRecord */
206 3
        foreach ($zone as $resourceRecord) {
207 3
            $name = max($name, strlen($resourceRecord->getName() ?? ''));
208 3
            $ttl = max($ttl, strlen((string) $resourceRecord->getTtl()));
209 3
            $class = max($class, strlen($resourceRecord->getClass() ?? ''));
210 3
            $type = max($type, strlen($resourceRecord->getType() ?? ''));
211
        }
212
213
        return [
214 3
            $name,
215 3
            $ttl,
216 3
            $type,
217 3
            $class,
218 3
            $name + $ttl + $class + $type + 4,
219
        ];
220
    }
221
}
222