001/*
002 * Copyright 2007-2021 The jdeb developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.vafer.jdeb.debian;
018
019import java.io.BufferedReader;
020import java.io.ByteArrayInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.text.ParseException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033import static java.nio.charset.StandardCharsets.*;
034
035/**
036 * A control file as specified by the <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html">Debian policy</a>.
037 */
038public abstract class ControlFile {
039
040    protected final Map<String, String> values = new LinkedHashMap<>();
041    protected final Map<String, String> userDefinedFields = new LinkedHashMap<>();
042    protected final Set<ControlField> userDefinedFieldNames = new HashSet<>();
043
044    public void parse(String input) throws IOException, ParseException {
045        parse(new ByteArrayInputStream(input.getBytes(UTF_8)));
046    }
047
048    public void parse(InputStream input) throws IOException, ParseException {
049        BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8));
050        StringBuilder buffer = new StringBuilder();
051        String field = null;
052        int linenr = 0;
053        while (true) {
054            final String line = reader.readLine();
055
056            if (line == null) {
057                // flush value of the previous field
058                set(field, buffer.toString());
059                break;
060            }
061
062            linenr++;
063
064            if (line.length() == 0) {
065                throw new ParseException("Empty line", linenr);
066            }
067
068            final char first = line.charAt(0);
069            if (first == '#') {
070                // ignore commented out lines
071                continue;
072            }
073
074            if (Character.isLetter(first)) {
075
076                // new field
077
078                // flush value of the previous field
079                set(field, buffer.toString());
080                buffer = new StringBuilder();
081
082
083                final int i = line.indexOf(':');
084
085                if (i < 0) {
086                    throw new ParseException("Line misses ':' delimiter", linenr);
087                }
088
089                field = line.substring(0, i);
090                buffer.append(line.substring(i + 1).trim());
091
092                continue;
093            }
094
095            // continuing old value, lines with only a dot are ignored
096            buffer.append('\n');
097            if (!".".equals(line.substring(1).trim())) {
098                buffer.append(line.substring(1));
099            }
100        }
101        reader.close();
102
103    }
104
105    public void set(String field, final String value) {
106        if (field != null && isUserDefinedField(field)) {
107            userDefinedFields.put(field, value);
108            String fieldName = getUserDefinedFieldName(field);
109
110            if (fieldName != null) {
111                userDefinedFieldNames.add(new ControlField(fieldName));
112            }
113
114            field = fieldName;
115        }
116
117        if (field != null && !"".equals(field)) {
118            values.put(field, value);
119        }
120    }
121
122    public String get(String field) {
123        return values.get(field);
124    }
125
126    protected abstract ControlField[] getFields();
127
128    protected Map<String, String> getUserDefinedFields() {
129        return userDefinedFields;
130    }
131
132    protected Set<ControlField> getUserDefinedFieldNames() {
133        return userDefinedFieldNames;
134    }
135
136    public List<String> getMandatoryFields() {
137        List<String> fields = new ArrayList<>();
138
139        for (ControlField field : getFields()) {
140            if (field.isMandatory()) {
141                fields.add(field.getName());
142            }
143        }
144
145        return fields;
146    }
147
148    public boolean isValid() {
149        return invalidFields().size() == 0;
150    }
151
152    public Set<String> invalidFields() {
153        Set<String> invalid = new HashSet<>();
154
155        for (ControlField field : getFields()) {
156            if (field.isMandatory() && get(field.getName()) == null) {
157                invalid.add(field.getName());
158            }
159        }
160
161        return invalid;
162    }
163
164    public String toString(ControlField... fields) {
165        StringBuilder s = new StringBuilder();
166        for (ControlField field : fields) {
167            String value = values.get(field.getName());
168            s.append(field.format(value));
169        }
170        return s.toString();
171    }
172
173    public String toString() {
174        List<ControlField> fields = new ArrayList<>();
175        fields.addAll(Arrays.asList(getFields()));
176        fields.addAll(getUserDefinedFieldNames());
177        return toString(fields.toArray(new ControlField[fields.size()]));
178    }
179
180    /**
181     * Returns the letter expected in the prefix of a user defined field
182     * in order to include the field in this control file.
183     *
184     * @return The letter returned is:
185     * <ul>
186     *   <li>B: for a binary package</li>
187     *   <li>S: for a source package</li>
188     *   <li>C: for a changes file</li>
189     * </ul>
190     *
191     * @since 1.1
192     * @see <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html#s5.7">Debian Policy - User-defined fields</a>
193     */
194    protected abstract char getUserDefinedFieldLetter();
195
196    /**
197     * Tells if the specified field name is a user defined field.
198     * User-defined fields must begin with an 'X', followed by one or more
199     * letters that specify the output file and a hyphen.
200     *
201     * @param field the name of the field
202     *
203     * @since 1.1
204     * @see <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html#s5.7">Debian Policy - User-defined fields</a>
205     */
206    protected boolean isUserDefinedField(String field) {
207        return field.startsWith("X") && field.indexOf("-") > 0;
208    }
209
210    /**
211     * Returns the user defined field without its prefix.
212     *
213     * @param field the name of the user defined field
214     * @return the user defined field without the prefix, or null if the fields
215     *         doesn't apply to this control file.
216     * @since 1.1
217     */
218    protected String getUserDefinedFieldName(String field) {
219        int index = field.indexOf('-');
220        char letter = getUserDefinedFieldLetter();
221
222        for (int i = 0; i < index; ++i) {
223            if (field.charAt(i) == letter) {
224                return field.substring(index + 1);
225            }
226        }
227
228        return null;
229    }
230}