# HG changeset patch # User David Barts # Date 1587782757 25200 # Node ID 3d86f03911681f43800395308d706e7e131c6dbd # Parent c06edc56669bbca35707d64283d8318924db1d68 Work on improving the build system. diff -r c06edc56669b -r 3d86f0391168 Building.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Building.html Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,63 @@ + + + + + + Building JpegWasher + + + +

Building JpegWasher

+

Prerequisites

+ +

JpegWasher Is Not Pure Java

+

This means two things:

+
    +
  1. You need a C++ compiler in addition to a Kotlin compiler (and a Java + one) to build  JpegWasher.
  2. +
  3. The result of a build will run only on the architecture you built it + on.
  4. +
+

It is possible to create a semi-portable JAR that runs on both + Linux and Windows (it contains both *.so libraries and *.dll + ones, and loads the correct ones at run time, see linwin.jar), + but the process for doing so is not totally automated. It is alas not + possible to support the Macintosh in the same JAR as well, because a Mac + app in Java 1.8 requires making a few nonportable, Apple-only API calls, + whose presence will cause ClassNotFoundException to be + thrown at runtime on non-Macintosh systems.

+

As to why, the answer is simple: Try as I could, I could not find any + pure Java libraries that could read and write image metadata. + Therefore I had to use a C++ library.

+

Which Version of Java to Use?

+

In short, Java 1.8. Most systems don't yet have OpenJDK 11 or greater + installed, so using a compiler newer than 1.8 is asking for trouble. All + code should build on OpenJDK 11 or greater, with the exception + of the OS-dependent code for the Macintosh (which will have to be recoded + to use the java.awt.Desktop class). The latter would be a + net win, as it is portable, and would spell the death of the only bit of + OS-dependent Kotlin code in this application.

+

In another year or two, I will probably make OpenJDK 11 or greater the + preferred version.

+ + diff -r c06edc56669b -r 3d86f0391168 Makefile.doc --- a/Makefile.doc Fri Apr 24 14:01:03 2020 -0700 +++ b/Makefile.doc Fri Apr 24 19:45:57 2020 -0700 @@ -1,6 +1,6 @@ .PHONY: clean -all: Readme.rst Readme.pdf +all: Readme.rst Readme.pdf Building.rst Building.pdf clean: -rm *.rst *.pdf *.ps *.nrf diff -r c06edc56669b -r 3d86f0391168 build.xml --- a/build.xml Fri Apr 24 14:01:03 2020 -0700 +++ b/build.xml Fri Apr 24 19:45:57 2020 -0700 @@ -1,5 +1,9 @@ - + @@ -17,15 +21,8 @@ - - - - - - + + @@ -49,6 +46,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + You can use the following targets: @@ -63,26 +120,40 @@ prompt> ant all + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + @@ -90,7 +161,7 @@ description="Clean work dirs, compile, make JAR."/> - diff -r c06edc56669b -r 3d86f0391168 lib/ant-contrib/ant-contrib-1.0b3.jar Binary file lib/ant-contrib/ant-contrib-1.0b3.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/ant-contrib/lib/bcel-5.1.jar Binary file lib/ant-contrib/lib/bcel-5.1.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/ant-contrib/lib/commons-httpclient-3.0.1.jar Binary file lib/ant-contrib/lib/commons-httpclient-3.0.1.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/ant-contrib/lib/commons-logging-1.0.4.jar Binary file lib/ant-contrib/lib/commons-logging-1.0.4.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/ant-contrib/lib/ivy-1.3.1.jar Binary file lib/ant-contrib/lib/ivy-1.3.1.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/jarbundler-core-3.3.0.jar Binary file lib/jarbundler-core-3.3.0.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/mod/build_mod.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/build_mod.xml Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + You can use the following targets: + + help : (default) Prints this message + clean : Deletes work directories + compile : Compiles source into class files + jar : Make JAR file. + + For example, to clean, compile, and package all at once, run: + prompt> ant -f build_mod.xml all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r c06edc56669b -r 3d86f0391168 lib/mod/mod.jar Binary file lib/mod/mod.jar has changed diff -r c06edc56669b -r 3d86f0391168 lib/mod/src/de/masters_of_disaster/ant/tasks/ar/Ar.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/Ar.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,468 @@ +package de.masters_of_disaster.ant.tasks.ar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.taskdefs.MatchingTask; +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.util.FileUtils; +import org.apache.tools.ant.util.MergingMapper; +import org.apache.tools.ant.util.SourceFileScanner; +import org.apache.tools.zip.UnixStat; + +/** + * Creates an ar archive. + * + * @ant.task category="packaging" + */ +public class Ar extends MatchingTask { + File destFile; + File baseDir; + + private ArLongFileMode longFileMode = new ArLongFileMode(); + + Vector filesets = new Vector(); + + /** + * Indicates whether the user has been warned about long files already. + */ + private boolean longWarningGiven = false; + + /** + * Add a new fileset with the option to specify permissions + * @return the ar fileset to be used as the nested element. + */ + public ArFileSet createArFileSet() { + ArFileSet fileset = new ArFileSet(); + filesets.addElement(fileset); + return fileset; + } + + + /** + * Set the name/location of where to create the ar file. + * @param destFile The output of the tar + */ + public void setDestFile(File destFile) { + this.destFile = destFile; + } + + /** + * This is the base directory to look in for things to ar. + * @param baseDir the base directory. + */ + public void setBasedir(File baseDir) { + this.baseDir = baseDir; + } + + /** + * Set how to handle long files, those with a name>16 chars or containing spaces. + * Optional, default=warn. + *

+ * Allowable values are + *

    + *
  • truncate - names are truncated to the maximum length, spaces are replaced by '_' + *
  • fail - names greater than the maximum cause a build exception + *
  • warn - names greater than the maximum cause a warning and TRUNCATE is used + *
  • bsd - BSD variant is used if any names are greater than the maximum. + *
  • gnu - GNU variant is used if any names are greater than the maximum. + *
  • omit - files with a name greater than the maximum are omitted from the archive + *
+ * @param mode the mode to handle long file names. + */ + public void setLongfile(ArLongFileMode mode) { + this.longFileMode = mode; + } + + /** + * do the business + * @throws BuildException on error + */ + public void execute() throws BuildException { + if (destFile == null) { + throw new BuildException("destFile attribute must be set!", + getLocation()); + } + + if (destFile.exists() && destFile.isDirectory()) { + throw new BuildException("destFile is a directory!", + getLocation()); + } + + if (destFile.exists() && !destFile.canWrite()) { + throw new BuildException("Can not write to the specified destFile!", + getLocation()); + } + + Vector savedFileSets = (Vector) filesets.clone(); + try { + if (baseDir != null) { + if (!baseDir.exists()) { + throw new BuildException("basedir does not exist!", + getLocation()); + } + + // add the main fileset to the list of filesets to process. + ArFileSet mainFileSet = new ArFileSet(fileset); + mainFileSet.setDir(baseDir); + filesets.addElement(mainFileSet); + } + + if (filesets.size() == 0) { + throw new BuildException("You must supply either a basedir " + + "attribute or some nested filesets.", + getLocation()); + } + + // check if ar is out of date with respect to each + // fileset + boolean upToDate = true; + for (Enumeration e = filesets.elements(); e.hasMoreElements();) { + ArFileSet fs = (ArFileSet) e.nextElement(); + String[] files = fs.getFiles(getProject()); + + if (!archiveIsUpToDate(files, fs.getDir(getProject()))) { + upToDate = false; + } + + for (int i = 0; i < files.length; ++i) { + if (destFile.equals(new File(fs.getDir(getProject()), + files[i]))) { + throw new BuildException("An ar file cannot include " + + "itself", getLocation()); + } + } + } + + if (upToDate) { + log("Nothing to do: " + destFile.getAbsolutePath() + + " is up to date.", Project.MSG_INFO); + return; + } + + log("Building ar: " + destFile.getAbsolutePath(), Project.MSG_INFO); + + ArOutputStream aOut = null; + try { + aOut = new ArOutputStream( + new BufferedOutputStream( + new FileOutputStream(destFile))); + if (longFileMode.isTruncateMode() + || longFileMode.isWarnMode()) { + aOut.setLongFileMode(ArOutputStream.LONGFILE_TRUNCATE); + } else if (longFileMode.isFailMode() + || longFileMode.isOmitMode()) { + aOut.setLongFileMode(ArOutputStream.LONGFILE_ERROR); + } else if (longFileMode.isBsdMode()) { + aOut.setLongFileMode(ArOutputStream.LONGFILE_BSD); + } else { + // GNU + aOut.setLongFileMode(ArOutputStream.LONGFILE_GNU); + } + + longWarningGiven = false; + for (Enumeration e = filesets.elements(); + e.hasMoreElements();) { + ArFileSet fs = (ArFileSet) e.nextElement(); + String[] files = fs.getFiles(getProject()); + if (files.length > 1 && fs.getFullpath().length() > 0) { + throw new BuildException("fullpath attribute may only " + + "be specified for " + + "filesets that specify a " + + "single file."); + } + for (int i = 0; i < files.length; i++) { + File f = new File(fs.getDir(getProject()), files[i]); + arFile(f, aOut, fs); + } + } + } catch (IOException ioe) { + String msg = "Problem creating AR: " + ioe.getMessage(); + throw new BuildException(msg, ioe, getLocation()); + } finally { + FileUtils.close(aOut); + } + } finally { + filesets = savedFileSets; + } + } + + /** + * ar a file + * @param file the file to ar + * @param aOut the output stream + * @param arFileSet the fileset that the file came from. + * @throws IOException on error + */ + protected void arFile(File file, ArOutputStream aOut, ArFileSet arFileSet) + throws IOException { + FileInputStream fIn = null; + + if (file.isDirectory()) { + return; + } + + String fileName = file.getName(); + + String fullpath = arFileSet.getFullpath(); + if (fullpath.length() > 0) { + fileName = fullpath.substring(fullpath.lastIndexOf('/')); + } + + // don't add "" to the archive + if (fileName.length() <= 0) { + return; + } + + try { + if ((fileName.length() >= ArConstants.NAMELEN) + || (-1 != fileName.indexOf(' '))) { + if (longFileMode.isOmitMode()) { + log("Omitting: " + fileName, Project.MSG_INFO); + return; + } else if (longFileMode.isWarnMode()) { + if (!longWarningGiven) { + log("Resulting ar file contains truncated or space converted filenames", + Project.MSG_WARN); + longWarningGiven = true; + } + log("Entry: \"" + fileName + "\" longer than " + + ArConstants.NAMELEN + " characters or containing spaces.", + Project.MSG_WARN); + } else if (longFileMode.isFailMode()) { + throw new BuildException("Entry: \"" + fileName + + "\" longer than " + ArConstants.NAMELEN + + "characters or containting spaces.", getLocation()); + } + } + + ArEntry ae = new ArEntry(fileName); + ae.setFileDate(file.lastModified()); + ae.setUserId(arFileSet.getUid()); + ae.setGroupId(arFileSet.getGid()); + ae.setMode(arFileSet.getMode()); + ae.setSize(file.length()); + + aOut.putNextEntry(ae); + + fIn = new FileInputStream(file); + + byte[] buffer = new byte[8 * 1024]; + int count = 0; + do { + aOut.write(buffer, 0, count); + count = fIn.read(buffer, 0, buffer.length); + } while (count != -1); + + aOut.closeEntry(); + } finally { + if (fIn != null) { + fIn.close(); + } + } + } + + /** + * Is the archive up to date in relationship to a list of files. + * @param files the files to check + * @param dir the base directory for the files. + * @return true if the archive is up to date. + */ + protected boolean archiveIsUpToDate(String[] files, File dir) { + SourceFileScanner sfs = new SourceFileScanner(this); + MergingMapper mm = new MergingMapper(); + mm.setTo(destFile.getAbsolutePath()); + return sfs.restrict(files, dir, null, mm).length == 0; + } + + /** + * This is a FileSet with the option to specify permissions + * and other attributes. + */ + public static class ArFileSet extends FileSet { + private String[] files = null; + + private int fileMode = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; + private int uid; + private int gid; + private String fullpath = ""; + + /** + * Creates a new ArFileSet instance. + * Using a fileset as a constructor argument. + * + * @param fileset a FileSet value + */ + public ArFileSet(FileSet fileset) { + super(fileset); + } + + /** + * Creates a new ArFileSet instance. + * + */ + public ArFileSet() { + super(); + } + + /** + * Get a list of files and directories specified in the fileset. + * @param p the current project. + * @return a list of file and directory names, relative to + * the baseDir for the project. + */ + public String[] getFiles(Project p) { + if (files == null) { + DirectoryScanner ds = getDirectoryScanner(p); + files = ds.getIncludedFiles(); + } + + return files; + } + + /** + * A 3 digit octal string, specify the user, group and + * other modes in the standard Unix fashion; + * optional, default=0644 + * @param octalString a 3 digit octal string. + */ + public void setMode(String octalString) { + this.fileMode = + UnixStat.FILE_FLAG | Integer.parseInt(octalString, 8); + } + + /** + * @return the current mode. + */ + public int getMode() { + return fileMode; + } + + /** + * The UID for the ar entry; optional, default="0" + * @param uid the id of the user for the ar entry. + */ + public void setUid(int uid) { + this.uid = uid; + } + + /** + * @return the UID for the ar entry + */ + public int getUid() { + return uid; + } + + /** + * The GID for the ar entry; optional, default="0" + * @param gid the group id. + */ + public void setGid(int gid) { + this.gid = gid; + } + + /** + * @return the group identifier. + */ + public int getGid() { + return gid; + } + + /** + * If the fullpath attribute is set, the file in the fileset + * is written with the last part of the path in the archive. + * If the fullpath ends in '/' the file is omitted from the archive. + * It is an error to have more than one file specified in such a fileset. + * @param fullpath the path to use for the file in a fileset. + */ + public void setFullpath(String fullpath) { + this.fullpath = fullpath; + } + + /** + * @return the path to use for a single file fileset. + */ + public String getFullpath() { + return fullpath; + } + } + + /** + * Set of options for long file handling in the task. + */ + public static class ArLongFileMode extends EnumeratedAttribute { + /** permissible values for longfile attribute */ + public static final String + WARN = "warn", + FAIL = "fail", + TRUNCATE = "truncate", + GNU = "gnu", + BSD = "bsd", + OMIT = "omit"; + + private final String[] validModes = {WARN, FAIL, TRUNCATE, GNU, BSD, OMIT}; + + /** Constructor, defaults to "warn" */ + public ArLongFileMode() { + super(); + setValue(WARN); + } + + /** + * @return the possible values for this enumerated type. + */ + public String[] getValues() { + return validModes; + } + + /** + * @return true if value is "truncate". + */ + public boolean isTruncateMode() { + return TRUNCATE.equalsIgnoreCase(getValue()); + } + + /** + * @return true if value is "warn". + */ + public boolean isWarnMode() { + return WARN.equalsIgnoreCase(getValue()); + } + + /** + * @return true if value is "gnu". + */ + public boolean isGnuMode() { + return GNU.equalsIgnoreCase(getValue()); + } + + /** + * @return true if value is "bsd". + */ + public boolean isBsdMode() { + return BSD.equalsIgnoreCase(getValue()); + } + + /** + * @return true if value is "fail". + */ + public boolean isFailMode() { + return FAIL.equalsIgnoreCase(getValue()); + } + + /** + * @return true if value is "omit". + */ + public boolean isOmitMode() { + return OMIT.equalsIgnoreCase(getValue()); + } + } +} diff -r c06edc56669b -r 3d86f0391168 lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArConstants.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArConstants.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,62 @@ +package de.masters_of_disaster.ant.tasks.ar; + +/** + * This interface contains all the definitions used in the package. + */ + +public interface ArConstants { + /** + * The length of the name field in a file header. + */ + int NAMELEN = 16; + + /** + * The length of the file date field in a file header. + */ + int FILEDATELEN = 12; + + /** + * The length of the user id field in a file header. + */ + int UIDLEN = 6; + + /** + * The length of the group id field in a file header. + */ + int GIDLEN = 6; + + /** + * The length of the mode field in a file header. + */ + int MODELEN = 8; + + /** + * The length of the size field in a file header. + */ + int SIZELEN = 10; + + /** + * The length of the magic field in a file header. + */ + int MAGICLEN = 2; + + /** + * The magic tag put at the end of a file header. + */ + String HEADERMAGIC = "`\n"; + + /** + * The headerlength of a file header. + */ + int HEADERLENGTH = NAMELEN + FILEDATELEN + UIDLEN + GIDLEN + MODELEN + SIZELEN + MAGICLEN; + + /** + * The length of the magic field in a file header. + */ + byte[] PADDING = { '\n' }; + + /** + * The magic tag representing an ar archive. + */ + byte[] ARMAGIC = { '!', '<', 'a', 'r', 'c', 'h', '>', '\n' }; +} diff -r c06edc56669b -r 3d86f0391168 lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArEntry.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArEntry.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,355 @@ +package de.masters_of_disaster.ant.tasks.ar; + +import java.io.File; +import java.util.Date; + +/** + * This class represents an entry in an Ar archive. It consists + * of the entry's header, as well as the entry's File. Entries + * can be instantiated in one of three ways, depending on how + * they are to be used. + *

+ * ArEntries that are created from the header bytes read from + * an archive are instantiated with the ArEntry( byte[] ) + * constructor. These entries will be used when extracting from + * or listing the contents of an archive. These entries have their + * header filled in using the header bytes. They also set the File + * to null, since they reference an archive entry not a file. + *

+ * ArEntries that are created from Files that are to be written + * into an archive are instantiated with the ArEntry( File ) + * constructor. These entries have their header filled in using + * the File's information. They also keep a reference to the File + * for convenience when writing entries. + *

+ * Finally, ArEntries can be constructed from nothing but a name. + * This allows the programmer to construct the entry by hand, for + * instance when only an InputStream is available for writing to + * the archive, and the header information is constructed from + * other information. In this case the header fields are set to + * defaults and the File is set to null. + * + *

+ * The C structure for an Ar Entry's header is: + *

+ * struct header {
+ * char filename[16];
+ * char filedate[12];
+ * char uid[6];
+ * char gid[6];
+ * char mode[8];
+ * char size[10];
+ * char magic[2];
+ * } header;
+ * 
+ * + */ + +public class ArEntry implements ArConstants { + /** The entry's filename. */ + private StringBuffer filename; + + /** The entry's file date. */ + private long fileDate; + + /** The entry's user id. */ + private int userId; + + /** The entry's group id. */ + private int groupId; + + /** The entry's permission mode. */ + private int mode; + + /** The entry's size. */ + private long size; + + /** The entry's magic tag. */ + private StringBuffer magic; + + /** The entry's file reference */ + private File file; + + /** Default permissions bits for files */ + public static final int DEFAULT_FILE_MODE = 0100644; + + /** Convert millis to seconds */ + public static final int MILLIS_PER_SECOND = 1000; + + /** + * Construct an empty entry and prepares the header values. + */ + private ArEntry () { + this.magic = new StringBuffer(HEADERMAGIC); + this.filename = new StringBuffer(); + this.userId = 0; + this.groupId = 0; + this.file = null; + } + + /** + * Construct an entry with only a name. This allows the programmer + * to construct the entry's header "by hand". File is set to null. + * + * @param name the entry name + */ + public ArEntry(String name) { + this(); + if (name.endsWith("/")) { + throw new IllegalArgumentException("ar archives can only contain files"); + } + this.filename = new StringBuffer(name); + this.mode = DEFAULT_FILE_MODE; + this.userId = 0; + this.groupId = 0; + this.size = 0; + this.fileDate = (new Date()).getTime() / MILLIS_PER_SECOND; + } + + /** + * Construct an entry for a file. File is set to file, and the + * header is constructed from information from the file. + * + * @param file The file that the entry represents. + */ + public ArEntry(File file) { + this(); + if (file.isDirectory()) { + throw new IllegalArgumentException("ar archives can only contain files"); + } + this.file = file; + this.filename = new StringBuffer(file.getName()); + this.fileDate = file.lastModified() / MILLIS_PER_SECOND; + this.mode = DEFAULT_FILE_MODE; + this.size = file.length(); + } + + /** + * Construct an entry from an archive's header bytes. File is set + * to null. + * + * @param headerBuf The header bytes from an ar archive entry. + */ + public ArEntry(byte[] headerBuf) { + this(); + this.parseArHeader(headerBuf); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(ArEntry it) { + return this.getFilename().equals(it.getFilename()); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(Object it) { + if (it == null || getClass() != it.getClass()) { + return false; + } + return equals((ArEntry) it); + } + + /** + * Hashcodes are based on entry names. + * + * @return the entry hashcode + */ + public int hashCode() { + return getFilename().hashCode(); + } + + /** + * Get this entry's name. + * + * @return This entry's name. + */ + public String getFilename() { + return this.filename.toString(); + } + + /** + * Set this entry's name. + * + * @param name This entry's new name. + */ + public void setFilename(String filename) { + this.filename = new StringBuffer(filename); + } + + /** + * Set the mode for this entry + * + * @param mode the mode for this entry + */ + public void setMode(int mode) { + this.mode = mode; + } + + /** + * Get this entry's user id. + * + * @return This entry's user id. + */ + public int getUserId() { + return this.userId; + } + + /** + * Set this entry's user id. + * + * @param userId This entry's new user id. + */ + public void setUserId(int userId) { + this.userId = userId; + } + + /** + * Get this entry's group id. + * + * @return This entry's group id. + */ + public int getGroupId() { + return this.groupId; + } + + /** + * Set this entry's group id. + * + * @param groupId This entry's new group id. + */ + public void setGroupId(int groupId) { + this.groupId = groupId; + } + + /** + * Convenience method to set this entry's group and user ids. + * + * @param userId This entry's new user id. + * @param groupId This entry's new group id. + */ + public void setIds(int userId, int groupId) { + this.setUserId(userId); + this.setGroupId(groupId); + } + + /** + * Set this entry's modification time. The parameter passed + * to this method is in "Java time". + * + * @param time This entry's new modification time. + */ + public void setFileDate(long time) { + this.fileDate = time / MILLIS_PER_SECOND; + } + + /** + * Set this entry's modification time. + * + * @param time This entry's new modification time. + */ + public void setFileDate(Date time) { + this.fileDate = time.getTime() / MILLIS_PER_SECOND; + } + + /** + * Get this entry's modification time. + * + * @return time This entry's new modification time. + */ + public Date getFileDate() { + return new Date(this.fileDate * MILLIS_PER_SECOND); + } + + /** + * Get this entry's file. + * + * @return This entry's file. + */ + public File getFile() { + return this.file; + } + + /** + * Get this entry's mode. + * + * @return This entry's mode. + */ + public int getMode() { + return this.mode; + } + + /** + * Get this entry's file size. + * + * @return This entry's file size. + */ + public long getSize() { + return this.size; + } + + /** + * Set this entry's file size. + * + * @param size This entry's new file size. + */ + public void setSize(long size) { + this.size = size; + } + + /** + * Write an entry's header information to a header buffer. + * + * @param outbuf The tar entry header buffer to fill in. + */ + public void writeEntryHeader(byte[] outbuf) { + int offset = 0; + + offset = ArUtils.getNameBytes(this.filename, outbuf, offset, NAMELEN); + offset = ArUtils.getLongBytes(this.fileDate, outbuf, offset, FILEDATELEN); + offset = ArUtils.getIntegerBytes(this.userId, outbuf, offset, UIDLEN); + offset = ArUtils.getIntegerBytes(this.groupId, outbuf, offset, GIDLEN); + offset = ArUtils.getOctalBytes(this.mode, outbuf, offset, MODELEN); + offset = ArUtils.getLongBytes(this.size, outbuf, offset, SIZELEN); + offset = ArUtils.getNameBytes(this.magic, outbuf, offset, MAGICLEN); + + while (offset < outbuf.length) { + outbuf[offset++] = 0; + } + } + + /** + * Parse an entry's header information from a header buffer. + * + * @param header The ar entry header buffer to get information from. + */ + public void parseArHeader(byte[] header) { + throw new UnsupportedOperationException("parseArHeader(byte[]) not yet implmented"); +// int offset = 0; +// +// this.filename = TarUtils.parseName(header, offset, NAMELEN); +// offset += NAMELEN; +// this.fileDate = TarUtils.parseOctal(header, offset, FILEDATELEN); +// offset += FILEDATELEN; +// this.userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); +// offset += UIDLEN; +// this.groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); +// offset += GIDLEN; +// this.mode = (int) TarUtils.parseOctal(header, offset, MODELEN); +// offset += MODELEN; +// this.size = TarUtils.parseOctal(header, offset, SIZELEN); +// offset += SIZELEN; +// this.magic = TarUtils.parseName(header, offset, MAGICLEN); +// offset += MAGICLEN; + } +} diff -r c06edc56669b -r 3d86f0391168 lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArOutputStream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArOutputStream.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,167 @@ +package de.masters_of_disaster.ant.tasks.ar; + +import java.io.FilterOutputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * The ArOutputStream writes an ar archive as an OutputStream. + * Methods are provided to put entries, and then write their contents + * by writing to this stream using write(). + */ +public class ArOutputStream extends FilterOutputStream { + /** Fail if a long file name is required in the archive or the name contains spaces. */ + public static final int LONGFILE_ERROR = 0; + + /** Long paths will be truncated in the archive. Spaces are replaced by '_' */ + public static final int LONGFILE_TRUNCATE = 1; + + /** GNU ar variant is used to store long file names and file names with spaced in the archive. */ + public static final int LONGFILE_GNU = 2; + + /** BSD ar variant is used to store long file names and file names with spaced in the archive. */ + public static final int LONGFILE_BSD = 3; + + protected int currSize; + protected int currBytes; + protected byte[] oneBuf; + protected int longFileMode = LONGFILE_ERROR; + protected boolean writingStarted = false; + protected boolean inEntry = false; + + public ArOutputStream(OutputStream os) throws IOException { + super(os); + if (null == os) { + throw new NullPointerException("os must not be null"); + } + this.out.write(ArConstants.ARMAGIC,0,ArConstants.ARMAGIC.length); + this.oneBuf = new byte[1]; + } + + public void setLongFileMode(int longFileMode) { + if (writingStarted) { + throw new IllegalStateException("longFileMode cannot be changed after writing to the archive has begun"); + } + if (LONGFILE_GNU == longFileMode) { + throw new UnsupportedOperationException("GNU variant isn't implemented yet"); + } + if (LONGFILE_BSD == longFileMode) { + throw new UnsupportedOperationException("BSD variant isn't implemented yet"); + } + this.longFileMode = longFileMode; + } + + /** + * Put an entry on the output stream. This writes the entry's + * header record and positions the output stream for writing + * the contents of the entry. Once this method is called, the + * stream is ready for calls to write() to write the entry's + * contents. Once the contents are written, closeEntry() + * MUST be called to ensure that all buffered data + * is completely written to the output stream. + * + * @param entry The ArEntry to be written to the archive. + */ + public void putNextEntry(ArEntry entry) throws IOException { + writingStarted = true; + if (inEntry) { + throw new IOException("the current entry has to be closed before starting a new one"); + } + String filename = entry.getFilename(); + if ((filename.length() >= ArConstants.NAMELEN) + && (longFileMode != LONGFILE_TRUNCATE)) { + throw new RuntimeException("file name \"" + entry.getFilename() + + "\" is too long ( > " + + ArConstants.NAMELEN + " bytes )"); + } + if (-1 != filename.indexOf(' ')) { + if (longFileMode == LONGFILE_TRUNCATE) { + entry.setFilename(filename.replace(' ','_')); + } else { + throw new RuntimeException("file name \"" + entry.getFilename() + + "\" contains spaces"); + } + } + + byte[] headerBuf = new byte[ArConstants.HEADERLENGTH]; + entry.writeEntryHeader(headerBuf); + this.out.write(headerBuf,0,ArConstants.HEADERLENGTH); + + this.currBytes = 0; + this.currSize = (int) entry.getSize(); + inEntry = true; + } + + /** + * Close an entry. This method MUST be called for all file + * entries that contain data. The reason is that we must + * pad an entries data if it is of odd size. + */ + public void closeEntry() throws IOException { + if (!inEntry) { + throw new IOException("we are not in an entry currently"); + } + + if (this.currBytes < this.currSize) { + throw new IOException("entry closed at '" + this.currBytes + + "' before the '" + this.currSize + + "' bytes specified in the header were written"); + } + + if (1 == (this.currSize & 1)) { + this.out.write(ArConstants.PADDING,0,1); + } + + inEntry = false; + } + + /** + * Writes a byte to the current ar archive entry. + * + * This method simply calls write( byte[], int, int ). + * + * @param b The byte to write to the archive. + */ + public void write(int b) throws IOException { + this.oneBuf[0] = (byte) b; + this.write(this.oneBuf, 0, 1); + } + + /** + * Writes bytes to the current ar archive entry. + * + * This method simply calls write( byte[], int, int ). + * + * @param wBuf The buffer to write to the archive. + */ + public void write(byte[] wBuf) throws IOException { + this.write(wBuf, 0, wBuf.length); + } + + /** + * Writes bytes to the current ar archive entry. This method + * is aware of the current entry and will throw an exception if + * you attempt to write bytes past the length specified for the + * current entry. + * + * @param wBuf The buffer to write to the archive. + * @param wOffset The offset in the buffer from which to get bytes. + * @param numToWrite The number of bytes to write. + */ + public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { + if (!inEntry) { + throw new IOException("we are not in an entry currently"); + } + + if ((this.currBytes + numToWrite) > this.currSize) { + throw new IOException("request to write '" + numToWrite + + "' bytes exceeds size in header of '" + + this.currSize + "' bytes"); + } + + if (numToWrite > 0) { + this.out.write(wBuf,wOffset,numToWrite); + this.currBytes += numToWrite; + } + } +} diff -r c06edc56669b -r 3d86f0391168 lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArUtils.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArUtils.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,155 @@ +package de.masters_of_disaster.ant.tasks.ar; + +/** + * This class provides static utility methods to work with byte streams. + */ +public class ArUtils { + /** + * Parse an octal string from a header buffer. This is used for the + * file permission mode value. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The long value of the octal string. + */ + public static long parseOctal(byte[] header, int offset, int length) { + long result = 0; + int end = offset + length; + + for (int i=offset ; i"); + } + + if (null != baseDir) { + // add the main fileset to the list of filesets to process. + fileSets.addElement(fileset); + } + + long realSize = 0; + long diskSize = 0; + for (Enumeration e=fileSets.elements() ; e.hasMoreElements() ; ) { + FileSet fileSet = (FileSet)e.nextElement(); + String[] files = fileSet.getDirectoryScanner(getProject()).getIncludedFiles(); + File fileSetDir = fileSet.getDir(getProject()); + for (int i=0, c=files.length ; i 0) && !fullpath.startsWith("./")) { + fileSet.setPrefix(""); + if (fullpath.startsWith("/")) { + fileSet.setFullpath("." + fullpath); + } else { + fileSet.setFullpath("./" + fullpath); + } + } + if ((0 == fileSet.getUid()) && ("" == fileSet.getUserName())) { + fileSet.setUserName("root"); + } + if ((0 == fileSet.getGid()) && ("" == fileSet.getGroup())) { + fileSet.setGroup("root"); + } + } + + for (Enumeration e=dataFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String prefix = fileSet.getPrefix(); + String fullpath = fileSet.getFullpath(); + if ("".equals(fullpath) && !prefix.startsWith("./")) { + if (prefix.startsWith("/")) { + fileSet.setPrefix("." + prefix); + } else { + fileSet.setPrefix("./" + prefix); + } + } + if ((fullpath.length() > 0) && !fullpath.startsWith("./")) { + fileSet.setPrefix(""); + if (fullpath.startsWith("/")) { + fileSet.setFullpath("." + fullpath); + } else { + fileSet.setFullpath("./" + fullpath); + } + } + if ((0 == fileSet.getUid()) && ("" == fileSet.getUserName())) { + fileSet.setUserName("root"); + } + if ((0 == fileSet.getGid()) && ("" == fileSet.getGroup())) { + fileSet.setGroup("root"); + } + } + + File md5sumsFile = new File(tempDir,"md5sums"); + if (includeMd5sums) { + Checksum md5 = new Checksum(); + prepareTask(md5); + int md5Count = 0; + StringBuffer md5sums = new StringBuffer(); + for (Enumeration e=dataFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String[] files = fileSet.getDirectoryScanner(getProject()).getIncludedFiles(); + File fileSetDir = fileSet.getDir(getProject()); + for (int i=0, c=files.length ; i 0) { + md5sums.append(fullpath.substring(2)); + } else { + md5sums.append(fileSet.getPrefix().substring(2)).append(files[i].replace('\\','/')); + } + md5sums.append("\n"); + md5Count++; + } + } + echo = new Echo(); + prepareTask(echo); + echo.setFile(md5sumsFile); + echo.setLevel(echoLevel); + echo.setMessage(md5sums.toString()); + echo.perform(); + tarFileSet = controlTarGz.createTarFileSet(); + tarFileSet.setFile(md5sumsFile); + tarFileSet.setUserName("root"); + tarFileSet.setGroup("root"); + tarFileSet.setPrefix("./"); + } + + TarCompressionMethod tarCompressionMethod = new TarCompressionMethod(); + tarCompressionMethod.setValue("gzip"); + controlTarGz.setCompression(tarCompressionMethod); + File controlTarGzFile = new File(tempDir,"control.tar.gz"); + controlTarGz.setDestFile(controlTarGzFile); + controlTarGz.perform(); + + dataTarGz.setCompression(tarCompressionMethod); + File dataTarGzFile = new File(tempDir,"data.tar.gz"); + dataTarGz.setDestFile(dataTarGzFile); + dataTarGz.perform(); + + FileUtils.delete(destFile); + ArFileSet fileSet = debPackage.createArFileSet(); + fileSet.setFile(debianBinaryFile); + fileSet = debPackage.createArFileSet(); + fileSet.setFile(controlTarGzFile); + fileSet = debPackage.createArFileSet(); + fileSet.setFile(dataTarGzFile); + debPackage.perform(); + + if (deleteTempFiles) { + FileUtils.delete(debianBinaryFile); + FileUtils.delete(controlTarGzFile); + FileUtils.delete(dataTarGzFile); + FileUtils.delete(md5sumsFile); + } + } + + /** + * Checks whether the package is up to date in relationship to a list of files. + * + * @param files the files to check + * @param dir the base directory for the files. + * @return true if the archive is up to date. + */ + protected boolean packageIsUpToDate(String[] files, File dir) { + SourceFileScanner sfs = new SourceFileScanner(this); + MergingMapper mm = new MergingMapper(); + mm.setTo(destFile.getAbsolutePath()); + return sfs.restrict(files, dir, null, mm).length == 0; + } + + /** + * Prepares a task for execution. + * + * @param task the task to prepare + */ + protected void prepareTask(Task task) { + task.setProject(getProject()); + task.setOwningTarget(getOwningTarget()); + task.setTaskName(getTaskName()); + task.setTaskType(getTaskType()); + } +} diff -r c06edc56669b -r 3d86f0391168 src/name/blackcap/exifwasher/Test.kt --- a/src/name/blackcap/exifwasher/Test.kt Fri Apr 24 14:01:03 2020 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -/* - * A basic test of the library: try to use it to print out the EXIF - * data. - */ -package name.blackcap.exifwasher - -import name.blackcap.exifwasher.exiv2.* - -/* entry point */ -fun main(args: Array) { - println("java.class.path = " + System.getProperty("java.class.path")) - if (args.size != 1) { - System.err.println("expecting a file name") - System.exit(1) - } - val image = Image(args[0]) - val meta = image.metadata - val keys = meta.keys - keys.sort() - keys.forEach { - val v = meta[it] - println("${it}: ${v.type} = ${v.value}") - } -} diff -r c06edc56669b -r 3d86f0391168 src/name/blackcap/exifwasher/Test2.kt --- a/src/name/blackcap/exifwasher/Test2.kt Fri Apr 24 14:01:03 2020 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/* - * A basic test of the library: try to use it to print out the EXIF - * data. - */ -package name.blackcap.exifwasher - -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import name.blackcap.exifwasher.exiv2.* - -/* entry point */ -fun main(args: Array) { - /* must have a file name */ - if (args.size != 1) { - System.err.println("expecting a file name") - System.exit(1) - } - - /* copy the file; we don't want to scribble on the original */ - val (name, ext) = splitext(args[0]) - val newName = "${name}_washed${ext}" - FileInputStream(args[0]).use { source -> - FileOutputStream(newName).use { target -> - source.copyTo(target) - } - } - - /* load the whitelist and image data */ - val white = Whitelist.parse(PROPERTIES.getProperty("whitelist")) - val image = Image(newName) - - /* do the washing */ - val meta = image.metadata - val keys = meta.keys - val keysin = keys.size - var deleted = 0 - keys.forEach { - if (!white.contains(it)) { - meta.erase(it) - deleted += 1 - } - } - - /* save and summarize */ - image.store() - println("${keysin} in - ${deleted} deleted = ${keysin - deleted} out") -} - -fun splitext(s: String): Pair { - val pos = s.lastIndexOf('.') - if (pos == -1) { - return Pair(s, "") - } - return Pair(s.substring(0, pos), s.substring(pos)) -}