net.sourceforge.javadpkg.impl.DebianPackageParserImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dpkg Show documentation
Show all versions of dpkg Show documentation
The library for reading and writing Debian Packages.
/*
* dpkg - Debian Package library and the Debian Package Maven plugin
* (c) Copyright 2015 Gerrit Hohl
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.sourceforge.javadpkg.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.zip.GZIPInputStream;
import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
import org.apache.commons.compress.archivers.ar.ArArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.CompressorInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import net.sourceforge.javadpkg.ChangeLog;
import net.sourceforge.javadpkg.ChangeLogParser;
import net.sourceforge.javadpkg.ConfigFiles;
import net.sourceforge.javadpkg.ConfigFilesParser;
import net.sourceforge.javadpkg.Context;
import net.sourceforge.javadpkg.Copyright;
import net.sourceforge.javadpkg.CopyrightParser;
import net.sourceforge.javadpkg.DebianPackage;
import net.sourceforge.javadpkg.DebianPackageConstants;
import net.sourceforge.javadpkg.DebianPackageParseHandler;
import net.sourceforge.javadpkg.DebianPackageParser;
import net.sourceforge.javadpkg.DocumentPaths;
import net.sourceforge.javadpkg.MD5Sums;
import net.sourceforge.javadpkg.MD5SumsParser;
import net.sourceforge.javadpkg.ParseException;
import net.sourceforge.javadpkg.Script;
import net.sourceforge.javadpkg.ScriptParser;
import net.sourceforge.javadpkg.SharedLibraries;
import net.sourceforge.javadpkg.SharedLibrariesParser;
import net.sourceforge.javadpkg.Symbols;
import net.sourceforge.javadpkg.SymbolsParser;
import net.sourceforge.javadpkg.Templates;
import net.sourceforge.javadpkg.TemplatesParser;
import net.sourceforge.javadpkg.control.BinaryControl;
import net.sourceforge.javadpkg.control.Control;
import net.sourceforge.javadpkg.control.ControlParser;
import net.sourceforge.javadpkg.control.impl.ControlParserImpl;
import net.sourceforge.javadpkg.io.DataSource;
import net.sourceforge.javadpkg.io.FileMetaData;
import net.sourceforge.javadpkg.io.FileMode;
import net.sourceforge.javadpkg.io.FileOwner;
import net.sourceforge.javadpkg.io.impl.DataStreamSource;
import net.sourceforge.javadpkg.io.impl.FileMetaDataImpl;
import net.sourceforge.javadpkg.io.impl.FileModeImpl;
import net.sourceforge.javadpkg.io.impl.FileOwnerImpl;
import net.sourceforge.javadpkg.io.impl.UncloseableInputStream;
/**
*
* A {@link DebianPackageParser} implementation.
*
*
* This implementation currently only supports Debian binary packages.
*
*
* @author Gerrit Hohl ([email protected])
* @version 1.0, 27.12.2015 by Gerrit Hohl
*/
public class DebianPackageParserImpl implements DebianPackageParser, DebianPackageConstants {
/** The parser for the control. */
private ControlParser controlParser;
/** The parser for the MD5 sums. */
private MD5SumsParser md5SumsParser;
/** The parser for scripts. */
private ScriptParser scriptParser;
/** The parser for the templates. */
private TemplatesParser templatesParser;
/** The parser for the configuration files. */
private ConfigFilesParser configFilesParser;
/** The parser for the shared libraries. */
private SharedLibrariesParser sharedLibrariesParser;
/** The parser for the symbols. */
private SymbolsParser symbolsParser;
/** The parser for the copyright. */
private CopyrightParser copyrightParser;
/** The parser for the change log. */
private ChangeLogParser changeLogParser;
/**
*
* Creates a parser.
*
*/
protected DebianPackageParserImpl() {
super();
this.controlParser = new ControlParserImpl();
this.md5SumsParser = new MD5SumsParserImpl();
this.scriptParser = new ScriptParserImpl();
this.templatesParser = new TemplatesParserImpl();
this.configFilesParser = new ConfigFilesParserImpl();
this.sharedLibrariesParser = new SharedLibrariesParserImpl();
this.symbolsParser = new SymbolsParserImpl();
this.copyrightParser = new CopyrightParserImpl();
this.changeLogParser = new ChangeLogParserImpl();
}
@Override
public DebianPackage parseDebianPackage(DataSource source, Context context) throws IOException, ParseException {
DebianPackage debianPackage;
DebianPackageParseHandler handler;
if (source == null)
throw new IllegalArgumentException("Argument source is null.");
if (context == null)
throw new IllegalArgumentException("Argument context is null.");
handler = new DebianPackageParseHandlerImpl();
debianPackage = this.parseDebianPackage(source, handler, context);
return debianPackage;
}
@Override
public DebianPackage parseDebianPackage(DataSource source, DebianPackageParseHandler handler, Context context)
throws IOException, ParseException {
DebianPackageImpl debianPackage;
if (source == null)
throw new IllegalArgumentException("Argument source is null.");
if (handler == null)
throw new IllegalArgumentException("Argument handler is null.");
if (context == null)
throw new IllegalArgumentException("Argument context is null.");
debianPackage = new DebianPackageImpl();
try (InputStream in = source.getInputStream()) {
try (ArArchiveInputStream arIn = new ArArchiveInputStream(in)) {
// --- Read the version of the package ---
this.readVersion(debianPackage, source, arIn);
// --- Read the meta information ---
this.readControl(debianPackage, source, context, arIn);
// --- Read the contents ---
this.readData(handler, debianPackage, source, context, arIn);
}
}
return debianPackage;
}
/**
*
* Reads the version of the Debian package.
*
*
* @param debianPackage
* The Debian package.
* @param source
* The source.
* @param in
* The stream on the archive.
* @throws IOException
* If an I/O error occurs.
* @throws ParseException
* If an error occurs during the parsing.
*/
private void readVersion(DebianPackageImpl debianPackage, DataSource source, ArArchiveInputStream in)
throws IOException, ParseException {
ArArchiveEntry entry;
String line;
entry = in.getNextArEntry();
if (!DEBIAN_BINARY.equals(entry.getName()))
throw new ParseException(
"Couldn't find entry |" + DEBIAN_BINARY + "| in AR archive |" + source.getName() + "|: " + entry.getName());
try {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new UncloseableInputStream(in), UTF_8_CHARSET))) {
line = reader.readLine();
}
} catch (IOException e) {
throw new IOException("Couldn't read entry |" + entry.getName() + "| from AR archive |" + source.getName() + "|: "
+ e.getMessage());
}
if (line == null)
throw new ParseException(
"Couldn't read content of |" + DEBIAN_BINARY + "| in AR archive |" + source.getName() + "|.");
// TODO Add a warning if the version is not "2.0" as we currently only support this version.
debianPackage.setFileFormatVersion(line);
}
/**
*
* Reads the meta information of the Debian package.
*
*
* @param debianPackage
* The Debian package.
* @param source
* The source.
* @param context
* The context.
* @param in
* The stream on the archive.
* @throws IOException
* If an I/O error occurs.
* @throws ParseException
* If an error occurs during the parsing.
*/
private void readControl(DebianPackageImpl debianPackage, DataSource source, Context context, ArArchiveInputStream in)
throws IOException, ParseException {
ArArchiveEntry entry;
TarArchiveEntry tarEntry;
String name;
Control control = null;
MD5Sums md5Sums = null;
Script preInstall = null, postInstall = null, preRemove = null, postRemove = null, config = null;
Templates templates = null;
ConfigFiles configFiles = null;
SharedLibraries sharedLibraries = null;
Symbols symbols = null;
entry = in.getNextArEntry();
if (!entry.getName().startsWith(CONTROL_TAR_PREFIX))
throw new ParseException("Couldn't find entry |" + CONTROL_TAR_PREFIX + "*| in AR archive |" + source.getName()
+ "|. Found |" + entry.getName() + "| instead.");
try {
try (TarArchiveInputStream tarIn = this.openTarArchive(in, entry.getName())) {
while ((tarEntry = tarIn.getNextTarEntry()) != null) {
name = tarEntry.getName();
// --- Remove leading path ---
if (name.startsWith("./")) {
name = name.substring(2);
}
// --- Ignore empty names (the root directory of the archive) ---
if (name.isEmpty()) {
continue;
}
switch (name) {
case CONTROL_ENTRY:
if (control != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
control = this.controlParser.parseControl(entrySource, context);
debianPackage.setControl(control);
}
break;
case MD5SUMS_ENTRY:
// TODO Warn if no MD5 sums are available.
if (md5Sums != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
md5Sums = this.md5SumsParser.parseMD5Sums(entrySource, context);
debianPackage.setMD5Sums(md5Sums);
}
break;
case PREINST_ENTRY:
if (preInstall != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
preInstall = this.scriptParser.parseScript(entrySource, context);
debianPackage.setPreInstall(preInstall);
}
break;
case POSTINST_ENTRY:
if (postInstall != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
postInstall = this.scriptParser.parseScript(entrySource, context);
debianPackage.setPostInstall(postInstall);
}
break;
case PRERM_ENTRY:
if (preRemove != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
preRemove = this.scriptParser.parseScript(entrySource, context);
debianPackage.setPreRemove(preRemove);
}
break;
case POSTRM_ENTRY:
if (postRemove != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
postRemove = this.scriptParser.parseScript(entrySource, context);
debianPackage.setPostRemove(postRemove);
}
break;
case TEMPLATES_ENTRY:
if (templates != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
templates = this.templatesParser.parseTemplates(entrySource, context);
debianPackage.setTemplates(templates);
}
break;
case CONFIG_ENTRY:
if (config != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
config = this.scriptParser.parseScript(entrySource, context);
debianPackage.setConfig(config);
}
break;
case CONFFILES_ENTRY:
if (configFiles != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
configFiles = this.configFilesParser.parseConfigFiles(entrySource, context);
debianPackage.setConfigFiles(configFiles);
}
break;
case SHLIBS_ENTRY:
if (sharedLibraries != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
sharedLibraries = this.sharedLibrariesParser.parseSharedLibraries(entrySource, context);
debianPackage.setSharedLibraries(sharedLibraries);
}
break;
case SYMBOLS_ENTRY:
if (symbols != null)
throw new ParseException("Found entry |" + name + "| in archive |" + entry.getName()
+ "| in source |" + source.getName() + "| more than one time.");
try (DataStreamSource entrySource = new DataStreamSource(tarIn, name, false)) {
symbols = this.symbolsParser.parseSymbols(entrySource, context);
debianPackage.setSymbols(symbols);
}
break;
// TODO Implement "triggers" in Debian package control.
default:
context.addWarning(new ControlUnsupportedEntryWarning(name));
}
}
}
} catch (IOException e) {
throw new IOException("Couldn't read entry |" + entry.getName() + "| from AR archive |" + source.getName() + "|: "
+ e.getMessage());
}
}
/**
*
* Reads the contents of the Debian package.
*
*
* @param handler
* The handler.
* @param debianPackage
* The Debian package.
* @param source
* The source.
* @param context
* The context.
* @param in
* The stream on the archive.
* @throws IOException
* If an I/O error occurs.
* @throws ParseException
* If an error occurs during the parsing.
*/
private void readData(DebianPackageParseHandler handler, DebianPackageImpl debianPackage, DataSource source,
Context context, ArArchiveInputStream in) throws IOException, ParseException {
ArArchiveEntry entry;
BinaryControl control;
DocumentPaths paths;
TarArchiveEntry tarEntry;
FileOwner owner;
FileMode mode;
FileMetaData file;
String name = null;
Copyright copyright;
ChangeLog changeLog;
boolean changeLogDebianRead = false;
entry = in.getNextArEntry();
if (!entry.getName().startsWith(DATA_TAR_PREFIX))
throw new ParseException("Couldn't find entry |" + DATA_TAR_PREFIX + "*| in AR archive |" + source.getName()
+ "|. Found |" + entry.getName() + "| instead.");
// --- Create the document paths ---
if (debianPackage.getControl() instanceof BinaryControl) {
control = (BinaryControl) debianPackage.getControl();
} else
throw new ParseException("Found control |" + debianPackage.getControl() + "| of type |"
+ (debianPackage.getControl() == null ? "null" : debianPackage.getControl().getClass().getCanonicalName())
+ ", but only control of type |" + BinaryControl.class.getCanonicalName() + "| is supported.");
paths = new DocumentPathsImpl(control.getPackage());
try {
try (TarArchiveInputStream tarIn = this.openTarArchive(in, entry.getName())) {
while ((tarEntry = tarIn.getNextTarEntry()) != null) {
// --- Get name without leading '.' ---
name = tarEntry.getName();
if (name.startsWith(".")) {
name = name.substring(1);
}
owner = new FileOwnerImpl(tarEntry.getLongGroupId(), tarEntry.getGroupName(), tarEntry.getLongUserId(),
tarEntry.getUserName());
mode = new FileModeImpl(tarEntry.getMode());
if (tarEntry.isDirectory()) {
file = FileMetaDataImpl.createDirectoryMetaData(name, owner, mode, tarEntry.getLastModifiedDate());
} else if (tarEntry.isFile()) {
file = FileMetaDataImpl.createFileMetaData(name, owner, mode, tarEntry.getSize(),
tarEntry.getLastModifiedDate());
} else if (tarEntry.isSymbolicLink()) {
file = FileMetaDataImpl.createSymbolicLinkMetaData(name, tarEntry.getLinkName(), owner, mode,
tarEntry.getLastModifiedDate());
} else
throw new IOException("Entry |" + name + "| is |" + this.getEntryType(tarEntry)
+ "| entry, but only entries of te types directory, file and symbolic link are supported.");
// TODO Memorize the files, their attributes and sizes.
// TODO Check MD5 sums (if available).
// --- Copyright and change log (if the file is a regular file) ---
if (file.isFile() && !file.isSymbolicLink()
&& (paths.isCopyrightPath(name) || paths.isChangeLogPath(name))) {
// --- Copyright ---
if (paths.isCopyrightPath(name)) {
if (debianPackage.getCopyright() != null)
throw new ParseException("Copyright is already set.");
copyright = this.readCopyright(tarIn, name, context);
debianPackage.setCopyright(copyright);
}
// --- Change log ---
else if (paths.isChangeLogPath(name)) {
if (tarEntry.getSize() > 0) {
if (debianPackage.getChangeLog() != null) {
/*
* --- Ignore the file if the Debian change log has already been read
* and the current file is the "normal" change log. ---
*/
if (changeLogDebianRead && !paths.isChangeLogDebianPath(name)) {
continue;
}
/*
* --- Otherwise throw an error if this is not the Debian change log
* or we have already read the Debian change log. ---
*/
else if (!paths.isChangeLogDebianPath(name))
throw new ParseException("Change log is already set.");
}
changeLog = this.readChangeLog(tarIn, name, paths, context);
debianPackage.setChangeLog(changeLog);
if (paths.isChangeLogDebianPath(name)) {
changeLogDebianRead = true;
}
} else {
context.addWarning(new CopyrightEmptyWarning());
}
} else if (!name.endsWith("/changelog-old.Debian.gz"))
throw new ParseException("Unsupported copyright or change log file: " + name);
} else {
if (file.isDirectory() || file.isSymbolicLink()) {
handler.handleData(file, null);
} else {
try (DataSource entrySource = new DataStreamSource(tarIn, name, false)) {
handler.handleData(file, entrySource);
}
}
}
}
}
// TODO Warn if no copyright, change log and/or MD5 sums have been found.
} catch (IOException e) {
throw new IOException("Couldn't read entry |" + entry.getName() + "| from AR archive |" + source.getName() + "|: "
+ e.getMessage(), e);
} catch (ParseException e) {
throw new ParseException("Couldn't parse |" + name + "| of entry |" + entry.getName() + "| from AR archive |"
+ source.getName() + "|: " + e.getMessage(), e);
}
}
/**
*
* Returns the type of the entry.
*
*
* @param entry
* The entry.
* @return The type.
*/
private String getEntryType(TarArchiveEntry entry) {
if (entry.isBlockDevice())
return "block device";
else if (entry.isCharacterDevice())
return "character device";
else if (entry.isDirectory())
return "directory";
else if (entry.isFIFO())
return "FIFO";
else if (entry.isLink())
return "link";
else if (entry.isSymbolicLink())
return "symbolic link";
return "unknown";
}
/**
*
* Reads the copyright from the stream.
*
*
* @param in
* The stream.
* @param name
* The name.
* @param context
* The context.
* @return The copyright.
* @throws IOException
* If an I/O error occurs.
* @throws ParseException
* If an error occurs during the parsing.
*/
private Copyright readCopyright(InputStream in, String name, Context context) throws IOException, ParseException {
Copyright copyright;
try (DataSource fileSource = new DataStreamSource(in, name, false)) {
copyright = this.copyrightParser.parseCopyright(fileSource, context);
}
return copyright;
}
/**
*
* Reads the change log from the stream.
*
*
* @param in
* The stream.
* @param name
* The name.
* @param paths
* The paths of the files in the document folder of the Debian
* package.
* @param context
* The context.
* @return The change log.
* @throws IOException
* If an I/O error occurs.
* @throws ParseException
* If an error occurs during the parsing.
*/
private ChangeLog readChangeLog(InputStream in, String name, DocumentPaths paths, Context context)
throws IOException, ParseException {
ChangeLog changeLog;
if (paths.isChangeLogGzipPath(name)) {
try (GZIPInputStream gzipIn = new GZIPInputStream(new UncloseableInputStream(in))) {
if (paths.isChangeLogHtmlPath(name)) {
try (DataSource fileSource = new DataStreamSource(gzipIn, name, false)) {
changeLog = this.changeLogParser.parseChangeLogHtml(fileSource, context);
}
} else {
try (DataSource fileSource = new DataStreamSource(gzipIn, name, false)) {
changeLog = this.changeLogParser.parseChangeLog(fileSource, context);
}
}
}
} else {
if (paths.isChangeLogHtmlPath(name)) {
try (DataSource fileSource = new DataStreamSource(in, name, false)) {
changeLog = this.changeLogParser.parseChangeLogHtml(fileSource, context);
}
} else {
try (DataSource fileSource = new DataStreamSource(in, name, false)) {
changeLog = this.changeLogParser.parseChangeLog(fileSource, context);
}
}
}
return changeLog;
}
/**
*
* Opens an entry with the specified name as TAR archive.
*
*
* @param in
* The underlying stream.
* @param name
* The name of the entry.
* @return The stream on the TAR archive.
* @throws IOException
* If an I/O error occurs while opening the TAR archive.
*/
@SuppressWarnings("resource")
private TarArchiveInputStream openTarArchive(InputStream in, String name) throws IOException {
UncloseableInputStream uncloseIn;
CompressorInputStream compressorIn;
TarArchiveInputStream tarIn;
uncloseIn = new UncloseableInputStream(in);
// TODO Memorize which format was used.
if (name.endsWith(TAR_GZIP_SUFFIX)) {
compressorIn = new GzipCompressorInputStream(uncloseIn);
} else if (name.endsWith(TAR_XZ_SUFFIX)) {
compressorIn = new XZCompressorInputStream(uncloseIn);
} else if (name.endsWith(TAR_BZIP2_SUFFIX)) {
compressorIn = new BZip2CompressorInputStream(uncloseIn);
} else
throw new IOException("Found entry |" + name + "| with unsupported compression.");
tarIn = new TarArchiveInputStream(compressorIn);
return tarIn;
}
/* **********************************************************************
* **********************************************************************
* **********************************************************************
* **********************************************************************
* **********************************************************************
*/
/**
*
* The {@link DebianPackageParseHandler} implementation of this class.
*
*
* @author Gerrit Hohl ([email protected])
* @version 1.0, 12.05.2016 by Gerrit Hohl
*/
private class DebianPackageParseHandlerImpl implements DebianPackageParseHandler {
/**
*
* Creates a handler.
*
*/
public DebianPackageParseHandlerImpl() {
super();
}
@Override
public void handleData(FileMetaData metaData, DataSource source) throws IOException, ParseException {
if (metaData == null)
throw new IllegalArgumentException("Argument metaData is null.");
if (metaData.isFile() && (source == null))
throw new IllegalArgumentException("Argument source for file |" + metaData.getAbsolutePath()
+ "| is null also the file is a regular file.");
// --- Do nothing. ---
}
}
}