From 3641e2586af0ade2720625fa8b007ea3a55f3514 Mon Sep 17 00:00:00 2001 From: Kin-Wai Koo Date: Thu, 22 Mar 2012 15:00:05 +0800 Subject: [PATCH] initial git commit --- LICENSE.txt | 22 + README.txt | 41 ++ src/cli/WhisperPrep.java | 175 ++++++++ src/gui/CustomJTable.java | 49 ++ src/gui/EXTHAddRecordListener.java | 8 + src/gui/GuiException.java | 14 + src/gui/GuiModel.java | 201 +++++++++ src/gui/HeaderInfoDialog.java | 73 +++ src/gui/LanguageCodes.java | 74 +++ src/gui/LanguageDialog.java | 258 +++++++++++ src/gui/LanguageModel.java | 12 + src/gui/Main.java | 667 ++++++++++++++++++++++++++++ src/gui/MetaInfoProvider.java | 6 + src/gui/MobiFileFilter.java | 32 ++ src/gui/NewRecordDialog.java | 175 ++++++++ src/mobimeta/EXTHHeader.java | 185 ++++++++ src/mobimeta/EXTHRecord.java | 251 +++++++++++ src/mobimeta/MobiCommon.java | 15 + src/mobimeta/MobiHeader.java | 526 ++++++++++++++++++++++ src/mobimeta/MobiMeta.java | 292 ++++++++++++ src/mobimeta/MobiMetaException.java | 14 + src/mobimeta/PDBHeader.java | 164 +++++++ src/mobimeta/RecordInfo.java | 41 ++ src/mobimeta/StreamUtils.java | 183 ++++++++ 24 files changed, 3478 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.txt create mode 100644 src/cli/WhisperPrep.java create mode 100644 src/gui/CustomJTable.java create mode 100644 src/gui/EXTHAddRecordListener.java create mode 100644 src/gui/GuiException.java create mode 100644 src/gui/GuiModel.java create mode 100644 src/gui/HeaderInfoDialog.java create mode 100644 src/gui/LanguageCodes.java create mode 100644 src/gui/LanguageDialog.java create mode 100644 src/gui/LanguageModel.java create mode 100644 src/gui/Main.java create mode 100644 src/gui/MetaInfoProvider.java create mode 100644 src/gui/MobiFileFilter.java create mode 100644 src/gui/NewRecordDialog.java create mode 100644 src/mobimeta/EXTHHeader.java create mode 100644 src/mobimeta/EXTHRecord.java create mode 100644 src/mobimeta/MobiCommon.java create mode 100644 src/mobimeta/MobiHeader.java create mode 100644 src/mobimeta/MobiMeta.java create mode 100644 src/mobimeta/MobiMetaException.java create mode 100644 src/mobimeta/PDBHeader.java create mode 100644 src/mobimeta/RecordInfo.java create mode 100644 src/mobimeta/StreamUtils.java diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..17e779a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2012 Kin-Wai Koo + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the ÒSoftwareÓ), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ÒAS ISÓ, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..1735ecd --- /dev/null +++ b/README.txt @@ -0,0 +1,41 @@ +You will need to have Java (JDK or JRE) 1.5 or above to execute this program. + +To start the program, just double-click on the MobiMetaEditorV0.15.jar file. + +If that doesn't work, type this from the command-line: + java -jar MobiMetaEditorV0.15.jar + +This was written based on the MOBI file format describe in: +http://wiki.mobileread.com/wiki/MOBI + +This application is licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php). + + +ChangeLog + +v0.15 +- added the ability to add/edit the TTS EXTH record + +v0.14 +- does not pack the header if the full name field and EXTH records remain unchanged +- added Open and Save menu items +- lets the user specify the target filename + +v0.13 +- added a Header Info dialog, which details various fields in the PDB header, PalmDOC header, and MOBI header +- added in provisions to invoke debug and safe modes from the command line +- added in WhisperPrep, which lets users set ASINs and sets the CDEType to EBOK + (to invoke, type "java -cp MobiMetaEditorV0.13.jar cli.WhisperPrep + ") + +v0.12 +- changed the GUI to use FileDialog instead of JFileChooser for a more native look and feel +- added support for window modified indicator on OS X +- lets the user specify an input filename on the command line + +v0.11 +- fixed some MobiHeader size calculation bugs +- added facilities to edit the language fields + +v0.10 +- initial release diff --git a/src/cli/WhisperPrep.java b/src/cli/WhisperPrep.java new file mode 100644 index 0000000..5ef42b4 --- /dev/null +++ b/src/cli/WhisperPrep.java @@ -0,0 +1,175 @@ +package cli; + +import java.util.List; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedList; + +import mobimeta.*; + +public class WhisperPrep +{ + private File inDir; + private File outDir; + private BufferedReader in; + + public final static void main(String[] args) + { + if (args.length != 2) printUsage(); + + File inDir = new File(args[0]); + File outDir = new File(args[1]); + + if (!inDir.exists() || !inDir.isDirectory()) + printUsage("Input directory " + args[0] + " does not exist."); + + if (!outDir.exists() || !outDir.isDirectory()) + printUsage("Output directory " + args[1] + " does not exist."); + + if (inDir.getAbsolutePath().equals(outDir.getAbsolutePath())) + printUsage("Input and output directories cannot be the same."); + + WhisperPrep wp = new WhisperPrep(inDir, outDir); + wp.start(); + } + + private static void printUsage() + { + printUsage(null); + } + + private static void printUsage(String message) + { + if (message != null) System.err.println(message); + System.err.println("Usage: WhisperPrep "); + System.exit(0); + } + + public WhisperPrep(File inDir, File outDir) + { + this.inDir = inDir; + this.outDir = outDir; + } + + public void start() + { + LinkedList fileList = new LinkedList(); + for (File f : inDir.listFiles()) + { + if (f.isFile() && f.getName().toLowerCase().endsWith(".mobi")) + fileList.add(f); + } + + in = new BufferedReader(new InputStreamReader(System.in)); + + int fileCount = fileList.size(); + int current = 1; + for (File f : fileList) + { + log("********************"); + log("File " + (current++) + "/" + fileCount); + log("Filename: " + f.getName()); + MobiMeta meta = null; + try + { + meta = new MobiMeta(f); + } + catch (MobiMetaException e) + { + log("Error: " + e.getMessage()); + continue; + } + + log("Fullname: " + meta.getFullName()); + List exthList = meta.getEXTHRecords(); + String encoding = meta.getCharacterEncoding(); + String author = null; + String isbn = null; + String oldasin = null; + for (EXTHRecord rec : exthList) + { + switch (rec.getRecordType()) + { + case 100: + author = StreamUtils.byteArrayToString(rec.getData(), encoding); + break; + + case 104: + isbn = StreamUtils.byteArrayToString(rec.getData(), encoding); + break; + + case 113: + oldasin = StreamUtils.byteArrayToString(rec.getData(), encoding); + break; + + default: + break; + } + } + + if (author != null) log("Author: " + author); + if (isbn != null) log("ISBN: " + isbn); + String asin = null; + if (oldasin == null) + asin = getInput("ASIN: "); + else + asin = getInput("ASIN [" + oldasin + "]: "); + + if (asin.length() == 0) + { + if (oldasin != null) + asin = oldasin; + else + asin = null; + } + + for (EXTHRecord rec : exthList) + { + int recType = rec.getRecordType(); + if (recType == 113) + { + if (asin != null) exthList.remove(rec); + } + else if (recType == 501) + exthList.remove(rec); + } + + if (asin != null) exthList.add(new EXTHRecord(113, asin, encoding)); + exthList.add(new EXTHRecord(501, "EBOK", encoding)); + meta.setEXTHRecords(); + + try + { + meta.saveToNewFile(new File(outDir, f.getName())); + } + catch (MobiMetaException e) + { + log("Error saving file: " + e.getMessage()); + } + + log(""); + } + } + + private void log(String message) + { + System.out.println(message); + } + + private String getInput(String message) + { + System.out.print(message); + String value = null; + try + { + value = in.readLine(); + } + catch (IOException e) + { + } + + return (value == null)?"":value; + } +} diff --git a/src/gui/CustomJTable.java b/src/gui/CustomJTable.java new file mode 100644 index 0000000..c951ae3 --- /dev/null +++ b/src/gui/CustomJTable.java @@ -0,0 +1,49 @@ +package gui; + +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; + +public class CustomJTable extends JTable +{ + /** + * This code was adapted from + * http://www.javalobby.org/java/forums/t84905.html + * + * A traditional JTable lets you specify custom renderers and editors on a + * column by column basis. This class lets you have different renderers and + * editors in the same column (but different rows). + */ + private static final long serialVersionUID = 1L; + + public CustomJTable(TableModel model) + { + super(model); + } + + public TableCellRenderer getCellRenderer(int row, int column) + { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellRenderer renderer = tableColumn.getCellRenderer(); + if (renderer != null) return renderer; + if (getValueAt(row, column) != null) + { + return getDefaultRenderer(getValueAt(row, column).getClass()); + } + return getDefaultRenderer(getColumnClass(column)); + } + + public TableCellEditor getCellEditor(int row, int column) + { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellEditor editor = tableColumn.getCellEditor(); + if (editor != null) return editor; + if (getValueAt(row, column) != null) + { + return getDefaultEditor(getValueAt(row, column).getClass()); + } + return getDefaultEditor(getColumnClass(column)); + } +} diff --git a/src/gui/EXTHAddRecordListener.java b/src/gui/EXTHAddRecordListener.java new file mode 100644 index 0000000..bfcdcf2 --- /dev/null +++ b/src/gui/EXTHAddRecordListener.java @@ -0,0 +1,8 @@ +package gui; + +import mobimeta.EXTHRecord; + +public interface EXTHAddRecordListener +{ + public void addEXTHRecord(EXTHRecord rec); +} diff --git a/src/gui/GuiException.java b/src/gui/GuiException.java new file mode 100644 index 0000000..4d8113d --- /dev/null +++ b/src/gui/GuiException.java @@ -0,0 +1,14 @@ +package gui; + +public class GuiException extends Exception +{ + /** + * + */ + private static final long serialVersionUID = 1L; + + public GuiException(String message) + { + super(message); + } +} diff --git a/src/gui/GuiModel.java b/src/gui/GuiModel.java new file mode 100644 index 0000000..e76083f --- /dev/null +++ b/src/gui/GuiModel.java @@ -0,0 +1,201 @@ +package gui; + +import mobimeta.*; + +import java.io.File; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +class GuiModel extends AbstractTableModel implements EXTHAddRecordListener, LanguageModel, MetaInfoProvider +{ + /** + * This is essentially a proxy to the MobiMeta object. In addition, it + * serves as a TableModel for the JTable + */ + private static final long serialVersionUID = 1L; + + private MobiMeta model = null; + + // static globals for convenience + // + private static String fullName = null; + private static List exthRecords = null; + private static String characterEncoding = null; + + + public GuiModel() + { + } + + public void setModel(File f) throws GuiException + { + try + { + model = new MobiMeta(f); + } + catch (MobiMetaException e) + { + throw new GuiException(e.getMessage()); + } + + fullName = model.getFullName(); + exthRecords = model.getEXTHRecords(); + characterEncoding = model.getCharacterEncoding(); + } + + public String getColumnName(int col) + { + if (col == 0) + return "Record Type"; + else + return "Value"; + } + + public int getColumnCount() + { + return 2; + } + + public int getRowCount() + { + return (exthRecords == null)?0:exthRecords.size(); + } + + public Object getValueAt(int row, int col) + { + EXTHRecord rec = exthRecords.get(row); + int type = rec.getRecordType(); + String typeDesc = rec.getTypeDescription(); + + if (col == 0) + { + if (typeDesc == null) + return Integer.toString(type); + else + return type + " (" + typeDesc + ")"; + } + else + { + byte[] data = rec.getData(); + + if (typeDesc == null) + { + StringBuffer sb = new StringBuffer(); + sb.append("{ "); + int len = data.length; + for (int i=0; i 0) sb.append(", "); + sb.append(data[i] & 0xff); + } + sb.append(" }"); + return sb.toString(); + } + else if (EXTHRecord.isBooleanType(type)) + { + int value = StreamUtils.byteArrayToInt(data); + if (value == 0) + return Boolean.FALSE; + else + return Boolean.TRUE; + } + else + return StreamUtils.byteArrayToString(data, characterEncoding); + } + } + + public boolean isCellEditable(int row, int col) + { + if ((col == 0) || MobiCommon.safeMode) return false; + + return exthRecords.get(row).isKnownType(); + } + + public void setValueAt(Object value, int row, int column) + { + if (column != 1) return; + + if (value instanceof String) + exthRecords.get(row).setData((String)value, characterEncoding); + else if (value instanceof Boolean) + exthRecords.get(row).setData(((Boolean)value).booleanValue()); + else + return; + + fireTableCellUpdated(row, column); + } + + public String getFullName() + { + return fullName; + } + + public void setFullName(String s) + { + fullName = s; + } + + public void removeRecordAtRow(int row) + { + exthRecords.remove(row); + fireTableRowsDeleted(row, row); + } + + + public void addEXTHRecord(EXTHRecord rec) + { + exthRecords.add(rec); + int lastIndex = exthRecords.size() - 1; + fireTableRowsInserted(lastIndex, lastIndex); + } + + public static String getCharacterEncoding() + { + return characterEncoding; + } + + public void save(File outputFile, boolean packHeader) throws GuiException + { + if (packHeader) + { + model.setFullName(fullName); + model.setEXTHRecords(); + } + + try + { + model.saveToNewFile(outputFile, packHeader); + } + catch (MobiMetaException e) + { + throw new GuiException("Problems encountered while saving file: " + + e.getMessage()); + } + } + + public int getLocale() + { + return model.getLocale(); + } + + public int getDictInput() + { + return model.getDictInput(); + } + + public int getDictOutput() + { + return model.getDictOutput(); + } + + public void setLanguages(int locale, int dictInput, int dictOutput) + { + model.setLanguages(locale, dictInput, dictOutput); + } + + public String getMetaInfo() + { + return model.getMetaInfo(); + } +} diff --git a/src/gui/HeaderInfoDialog.java b/src/gui/HeaderInfoDialog.java new file mode 100644 index 0000000..80cf1ab --- /dev/null +++ b/src/gui/HeaderInfoDialog.java @@ -0,0 +1,73 @@ +package gui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import javax.swing.JTextArea; +import javax.swing.JScrollPane; + +public class HeaderInfoDialog extends JDialog implements ActionListener +{ + + /** + * + */ + private static final long serialVersionUID = 1L; + private final JPanel contentPanel = new JPanel(); + private JButton closeButton; + private JScrollPane scrollPane; + private JTextArea textArea; + + /** + * Create the dialog. + */ + public HeaderInfoDialog(JFrame parent, MetaInfoProvider infoProvider) + { + super(parent, "Header Info", true); + setBounds(100, 100, 450, 300); + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(new BorderLayout(0, 0)); + { + scrollPane = new JScrollPane(); + contentPanel.add(scrollPane, BorderLayout.CENTER); + { + textArea = new JTextArea(infoProvider.getMetaInfo()); + textArea.setEditable(false); + scrollPane.setViewportView(textArea); + } + } + { + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + { + closeButton = new JButton("Close"); + closeButton.addActionListener(this); + closeButton.setActionCommand("Cancel"); + buttonPane.add(closeButton); + } + } + } + + protected JButton getCloseButton() { + return closeButton; + } + + public void actionPerformed(ActionEvent e) + { + if (e.getSource() == closeButton) + { + setVisible(false); + dispose(); + } + } +} diff --git a/src/gui/LanguageCodes.java b/src/gui/LanguageCodes.java new file mode 100644 index 0000000..6f4f26a --- /dev/null +++ b/src/gui/LanguageCodes.java @@ -0,0 +1,74 @@ +package gui; + +public class LanguageCodes +{ + // these codes are taken from: + // http://www.autoitscript.com/autoit3/docs/appendix/OSLangCodes.htm + // + public final static int[] code = + { 0, 1078, 1052, 1025, 2049, 3073, 4097, 5121, 6145, 7169, 8193, 9217, + 10241, 11265, 12289, 13313, 14337, 15361, 16385, 1067, 1068, 2092, + 1069, 1059, 1026, 1027, 4, 1028, 2052, 3076, 4100, 5124, 1050, 1029, + 1030, 1043, 2067, 9, 1033, 2057, 3081, 4105, 5129, 6153, 7177, + 8201, 9225, 10249, 11273, 12297, 13321, 1061, 1080, 1073, 1035, + 1036, 2060, 3084, 4108, 5132, 6156, 1079, 1031, 2055, 3079, 4103, + 5127, 1032, 1037, 1081, 1038, 1039, 1057, 1040, 2064, 1041, 1087, + 1111, 1042, 1062, 1063, 1071, 1086, 2110, 1102, 1044, 2068, 1045, + 1046, 2070, 1048, 1049, 1103, 2074, 3098, 1051, 1060, 1034, 2058, + 3082, 4106, 5130, 6154, 7178, 8202, 9226, 10250, 11274, 12298, + 13322, 14346, 15370, 16394, 17418, 18442, 19466, 20490, 1089, 1053, + 2077, 1097, 1092, 1054, 1055, 1058, 1056, 1091, 2115, 1066 }; + + public final static String[] description = + { "Unspecified", "Afrikaans", "Albanian", "Arabic - Saudi Arabia", + "Arabic - Iraq", "Arabic - Egypt", "Arabic - Libya", + "Arabic - Algeria", "Arabic - Morocco", "Arabic - Tunisia", + "Arabic - Oman", "Arabic - Yemen", "Arabic - Syria", + "Arabic - Jordan", "Arabic - Lebanon", "Arabic - Kuwait", + "Arabic - UAE", "Arabic - Bahrain", "Arabic - Qatar", "Armenian", + "Azeri - Latin", "Azeri - Cyrillic", "Basque", "Belarusian", + "Bulgarian", "Catalan", "Chinese", "Chinese - Taiwan", "Chinese - PRC", + "Chinese - Hong Kong", "Chinese - Singapore", "Chinese - Macau", + "Croatian", "Czech", "Danish", "Dutch - Standard", + "Dutch - Belgian", "English", "English - United States", + "English Ð United Kingdom", "English - Australian", + "English - Canadian", "English - New Zealand", "English - Irish", + "English - South Africa", "English - Jamaica", + "English - Caribbean", "English - Belize", "English - Trinidad", + "English - Zimbabwe", "English - Philippines", "Estonian", + "Faeroese", "Farsi", "Finnish", "French - Standard", + "French - Belgian", "French - Canadian", "French - Swiss", + "French - Luxembourg", "French - Monaco", "Georgian", + "German - Standard", "German - Swiss", "German - Austrian", + "German - Luxembourg", "German - Liechtenstei", "Greek", "Hebrew", + "Hindi", "Hungarian", "Icelandic", "Indonesian", + "Italian - Standard", "Italian - Swiss", "Japanese", "Kazakh", + "Konkani", "Korean", "Latvian", "Lithuanian", "Macedonian", + "Malay - Malaysia", "Malay - Brunei Darussalam", "Marathi", + "Norwegian - Bokmal", "Norwegian - Nynorsk", "Polish", + "Portuguese - Brazilian", "Portuguese - Standard", "Romanian", + "Russian", "Sanskrit", "Serbian - Latin", "Serbian - Cyrillic", + "Slovak", "Slovenian", "Spanish - Traditional Sort", + "Spanish - Mexican", "Spanish - Modern Sort", + "Spanish - Guatemala", "Spanish - Costa Rica", "Spanish - Panama", + "Spanish - Dominican Republic", "Spanish - Venezuela", + "Spanish - Colombia", "Spanish - Peru", "Spanish - Argentina", + "Spanish - Ecuador", "Spanish - Chile", "Spanish - Uruguay", + "Spanish - Paraguay", "Spanish - Bolivia", "Spanish - El Salvador", + "Spanish - Honduras", "Spanish - Nicaragua", + "Spanish - Puerto Rico", "Swahili", "Swedish", "Swedish - Finland", + "Tamil", "Tatar", "Thai", "Turkish", "Ukrainian", "Urdu", + "Uzbek - Latin", "Uzbek - Cyrillic", "Vietnamese" }; + + + public static int codeToIndex(int key) + { + int len = code.length; + for (int i = 0; i < len; i++) + { + if (code[i] == key) return i; + } + + return -1; + } +} diff --git a/src/gui/LanguageDialog.java b/src/gui/LanguageDialog.java new file mode 100644 index 0000000..d6c2f5b --- /dev/null +++ b/src/gui/LanguageDialog.java @@ -0,0 +1,258 @@ +package gui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import java.awt.GridBagLayout; +import java.awt.Component; +import javax.swing.Box; +import javax.swing.JLabel; +import java.awt.GridBagConstraints; +import java.awt.Font; +import javax.swing.SwingConstants; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JComboBox; + +public class LanguageDialog extends JDialog implements ActionListener +{ + /** + * + */ + private static final long serialVersionUID = 1L; + + private final JPanel contentPanel = new JPanel(); + + private JButton okButton; + private JButton cancelButton; + private JComboBox cbLanguage; + private JComboBox cbDictInput; + private JComboBox cbDictOutput; + + // store original values here + private int locale; + private int dictInput; + private int dictOutput; + + // these values indicate if they are known values + private boolean localeExists; + private boolean dictInputExists; + private boolean dictOutputExists; + + private LanguageModel model; + + /** + * Create the dialog. + */ + public LanguageDialog(JFrame parent, LanguageModel model) + { + super(parent, true); + this.model = model; + locale = model.getLocale(); + dictInput = model.getDictInput(); + dictOutput = model.getDictOutput(); + localeExists = (LanguageCodes.codeToIndex(locale) != -1); + dictInputExists = (LanguageCodes.codeToIndex(dictInput) != -1); + dictOutputExists = (LanguageCodes.codeToIndex(dictOutput) != -1); + + String[] localeChoices; + String[] dictInputChoices; + String[] dictOutputChoices; + + // assemble choices for combo boxes + // + if (localeExists) + localeChoices = LanguageCodes.description; + else + { + localeChoices = new String[LanguageCodes.description.length + 1]; + System.arraycopy(LanguageCodes.description, 0, localeChoices, 0, + LanguageCodes.description.length); + localeChoices[localeChoices.length - 1] = "Unknown (" + locale + + ")"; + } + + if (dictInputExists) + dictInputChoices = LanguageCodes.description; + else + { + dictInputChoices = new String[LanguageCodes.description.length + 1]; + System.arraycopy(LanguageCodes.description, 0, dictInputChoices, 0, + LanguageCodes.description.length); + dictInputChoices[dictInputChoices.length - 1] = "Unknown (" + + dictInput + ")"; + } + + if (dictOutputExists) + dictOutputChoices = LanguageCodes.description; + else + { + dictOutputChoices = new String[LanguageCodes.description.length + 1]; + System.arraycopy(LanguageCodes.description, 0, dictOutputChoices, + 0, LanguageCodes.description.length); + dictOutputChoices[dictOutputChoices.length - 1] = "Unknown (" + + dictOutput + ")"; + } + + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setBounds(100, 100, 450, 300); + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(new BorderLayout(0, 0)); + { + Component verticalStrut = Box.createVerticalStrut(20); + contentPanel.add(verticalStrut, BorderLayout.NORTH); + } + { + Component verticalStrut = Box.createVerticalStrut(20); + contentPanel.add(verticalStrut, BorderLayout.SOUTH); + } + { + Component horizontalStrut = Box.createHorizontalStrut(20); + contentPanel.add(horizontalStrut, BorderLayout.WEST); + } + { + Component horizontalStrut = Box.createHorizontalStrut(20); + contentPanel.add(horizontalStrut, BorderLayout.EAST); + } + { + JPanel panel = new JPanel(); + contentPanel.add(panel, BorderLayout.CENTER); + GridBagLayout gbl_panel = new GridBagLayout(); + gbl_panel.columnWidths = new int[] + { 0, 0, 0 }; + gbl_panel.rowHeights = new int[] + { 0, 0, 0, 0 }; + gbl_panel.columnWeights = new double[] + { 0.0, 1.0, Double.MIN_VALUE }; + gbl_panel.rowWeights = new double[] + { 0.0, 0.0, 0.0, Double.MIN_VALUE }; + panel.setLayout(gbl_panel); + { + JLabel lblLanguage = new JLabel("Language"); + lblLanguage.setHorizontalAlignment(SwingConstants.RIGHT); + lblLanguage.setFont(new Font("Lucida Grande", Font.BOLD, 13)); + GridBagConstraints gbc_lblLanguage = new GridBagConstraints(); + gbc_lblLanguage.anchor = GridBagConstraints.EAST; + gbc_lblLanguage.insets = new Insets(0, 0, 5, 5); + gbc_lblLanguage.gridx = 0; + gbc_lblLanguage.gridy = 0; + panel.add(lblLanguage, gbc_lblLanguage); + } + { + cbLanguage = new JComboBox(localeChoices); + cbLanguage.setSelectedIndex(localeExists ? LanguageCodes + .codeToIndex(locale) : (localeChoices.length - 1)); + GridBagConstraints gbc_cbLanguage = new GridBagConstraints(); + gbc_cbLanguage.insets = new Insets(0, 0, 5, 0); + gbc_cbLanguage.fill = GridBagConstraints.HORIZONTAL; + gbc_cbLanguage.gridx = 1; + gbc_cbLanguage.gridy = 0; + panel.add(cbLanguage, gbc_cbLanguage); + } + { + JLabel lblDictionaryInput = new JLabel("Dictionary Input"); + lblDictionaryInput.setHorizontalAlignment(SwingConstants.RIGHT); + lblDictionaryInput.setFont(new Font("Lucida Grande", Font.BOLD, + 13)); + GridBagConstraints gbc_lblDictionaryInput = new GridBagConstraints(); + gbc_lblDictionaryInput.anchor = GridBagConstraints.EAST; + gbc_lblDictionaryInput.insets = new Insets(0, 0, 5, 5); + gbc_lblDictionaryInput.gridx = 0; + gbc_lblDictionaryInput.gridy = 1; + panel.add(lblDictionaryInput, gbc_lblDictionaryInput); + } + { + cbDictInput = new JComboBox(dictInputChoices); + cbDictInput + .setSelectedIndex(dictInputExists ? LanguageCodes + .codeToIndex(dictInput) + : (dictInputChoices.length - 1)); + GridBagConstraints gbc_cbDictInput = new GridBagConstraints(); + gbc_cbDictInput.insets = new Insets(0, 0, 5, 0); + gbc_cbDictInput.fill = GridBagConstraints.HORIZONTAL; + gbc_cbDictInput.gridx = 1; + gbc_cbDictInput.gridy = 1; + panel.add(cbDictInput, gbc_cbDictInput); + } + { + JLabel lblDictionaryOutput = new JLabel("Dictionary Output"); + lblDictionaryOutput + .setHorizontalAlignment(SwingConstants.RIGHT); + lblDictionaryOutput.setFont(new Font("Lucida Grande", + Font.BOLD, 13)); + GridBagConstraints gbc_lblDictionaryOutput = new GridBagConstraints(); + gbc_lblDictionaryOutput.anchor = GridBagConstraints.EAST; + gbc_lblDictionaryOutput.insets = new Insets(0, 0, 0, 5); + gbc_lblDictionaryOutput.gridx = 0; + gbc_lblDictionaryOutput.gridy = 2; + panel.add(lblDictionaryOutput, gbc_lblDictionaryOutput); + } + { + cbDictOutput = new JComboBox(dictOutputChoices); + cbDictOutput.setSelectedIndex(dictOutputExists ? LanguageCodes + .codeToIndex(dictOutput) + : (dictOutputChoices.length - 1)); + GridBagConstraints gbc_cbDictOutput = new GridBagConstraints(); + gbc_cbDictOutput.fill = GridBagConstraints.HORIZONTAL; + gbc_cbDictOutput.gridx = 1; + gbc_cbDictOutput.gridy = 2; + panel.add(cbDictOutput, gbc_cbDictOutput); + } + } + { + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + { + okButton = new JButton("OK"); + okButton.addActionListener(this); + buttonPane.add(okButton); + getRootPane().setDefaultButton(okButton); + } + { + cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(this); + buttonPane.add(cancelButton); + } + } + } + + public void actionPerformed(ActionEvent event) + { + Object source = event.getSource(); + + if (source == okButton) + { + int localeIndex = cbLanguage.getSelectedIndex(); + if ((localeIndex >= 0) && (localeIndex < LanguageCodes.code.length)) + locale = LanguageCodes.code[localeIndex]; + + int dictInputIndex = cbDictInput.getSelectedIndex(); + if ((dictInputIndex >= 0) && (dictInputIndex < LanguageCodes.code.length)) + dictInput = LanguageCodes.code[dictInputIndex]; + + int dictOutputIndex = cbDictOutput.getSelectedIndex(); + if ((dictOutputIndex >= 0) && (dictOutputIndex < LanguageCodes.code.length)) + dictOutput = LanguageCodes.code[dictOutputIndex]; + + model.setLanguages(locale, dictInput, dictOutput); + + setVisible(false); + dispose(); + } + else if (source == cancelButton) + { + setVisible(false); + dispose(); + } + } +} diff --git a/src/gui/LanguageModel.java b/src/gui/LanguageModel.java new file mode 100644 index 0000000..94ca6de --- /dev/null +++ b/src/gui/LanguageModel.java @@ -0,0 +1,12 @@ +package gui; + +public interface LanguageModel +{ + public int getLocale(); + + public int getDictInput(); + + public int getDictOutput(); + + public void setLanguages(int locale, int dictInput, int dictOutput); +} diff --git a/src/gui/Main.java b/src/gui/Main.java new file mode 100644 index 0000000..080a755 --- /dev/null +++ b/src/gui/Main.java @@ -0,0 +1,667 @@ +package gui; + +import java.io.*; +import java.util.HashSet; +import java.awt.EventQueue; + +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +import java.awt.GridBagLayout; +import javax.swing.JLabel; + +import java.awt.FileDialog; +import java.awt.GridBagConstraints; +import java.awt.Insets; +import java.awt.Rectangle; + +import javax.swing.JSeparator; +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.Component; +import javax.swing.Box; +import javax.swing.SwingConstants; +import java.awt.Font; +import javax.swing.JTextField; +import java.awt.Color; +import javax.swing.JScrollPane; +import java.awt.FlowLayout; +import javax.swing.JButton; +import javax.swing.JTable; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JTextArea; + +import mobimeta.MobiCommon; +import javax.swing.JMenuBar; +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +public class Main implements ListSelectionListener, ActionListener, + TableModelListener, LanguageModel +{ + private FileDialog openFileChooser = null; + private FileDialog saveFileChooser = null; + private JFrame frame; + private JTextArea lblInputFilename; + private JTextArea lblOutputFilename; + private JTextField tfFullName; + private JTable table; + private JButton buttonRemove; + private JButton buttonAdd; + private JButton buttonSave; + private JButton btnLanguage; + private JButton btnHeaderInfo; + private GuiModel model; + private File outputFile; + private boolean packHeader = false; + private JMenuItem mntmOpen; + private JMenuItem mntmSave; + + /** + * Launch the application. + */ + public static void main(String[] args) + { + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("com.apple.mrj.application.apple.menu.about.name", + "Mobi Meta Editor"); + + HashSet optionsSet = new HashSet(); + File inputFile = null; + for (int i=0; i row) + { + select = true; + } + else if (numRows > 0) + { + select = true; + row = numRows - 1; + } + if (select) + { + table.getSelectionModel().setSelectionInterval(row, row); + table.scrollRectToVisible(new Rectangle(table.getCellRect(row, + 0, true))); + } + packHeader = true; + setWindowChangedStatus(true); + } + else if (eventType == TableModelEvent.UPDATE) + { + packHeader = true; + setWindowChangedStatus(true); + } + } + + protected void setWindowChangedStatus(boolean status) + { + frame.getRootPane().putClientProperty("Window.documentModified", + Boolean.valueOf(status)); + } + + // we implement the LanguageModel interface because we want to intercept the + // setLanguages() call so that we can set the window status changed flag + // + public int getLocale() + { + return model.getLocale(); + } + + // we implement the LanguageModel interface because we want to intercept the + // setLanguages() call so that we can set the window status changed flag + // + public int getDictInput() + { + return model.getDictInput(); + } + + // we implement the LanguageModel interface because we want to intercept the + // setLanguages() call so that we can set the window status changed flag + // + public int getDictOutput() + { + return model.getDictOutput(); + } + + // we implement the LanguageModel interface because we want to intercept the + // setLanguages() call so that we can set the window status changed flag + // + public void setLanguages(int locale, int dictInput, int dictOutput) + { + model.setLanguages(locale, dictInput, dictOutput); + setWindowChangedStatus(true); + } + protected JMenuItem getMntmOpen() { + return mntmOpen; + } + protected JMenuItem getMntmSave() { + return mntmSave; + } +} diff --git a/src/gui/MetaInfoProvider.java b/src/gui/MetaInfoProvider.java new file mode 100644 index 0000000..da0c8e0 --- /dev/null +++ b/src/gui/MetaInfoProvider.java @@ -0,0 +1,6 @@ +package gui; + +public interface MetaInfoProvider +{ + public String getMetaInfo(); +} diff --git a/src/gui/MobiFileFilter.java b/src/gui/MobiFileFilter.java new file mode 100644 index 0000000..b29784a --- /dev/null +++ b/src/gui/MobiFileFilter.java @@ -0,0 +1,32 @@ +package gui; + +import java.io.File; +import java.io.FilenameFilter; + +import javax.swing.filechooser.FileFilter; + +class MobiFileFilter extends FileFilter implements FilenameFilter +{ + + // to make it work with JFileChooser + // + public boolean accept(File f) + { + if (f.isDirectory()) return true; + + return (f.getName().toLowerCase().endsWith(".azw") || f.getName().toLowerCase().endsWith(".mobi")); + } + + public String getDescription() + { + return "*.azw,*.mobi"; + } + + // to make it work with java.awt.FileDialog + // + public boolean accept(File f, String name) + { + return (name.toLowerCase().endsWith(".azw") || name.toLowerCase().endsWith(".mobi")); + } + +} diff --git a/src/gui/NewRecordDialog.java b/src/gui/NewRecordDialog.java new file mode 100644 index 0000000..00e027d --- /dev/null +++ b/src/gui/NewRecordDialog.java @@ -0,0 +1,175 @@ +package gui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import javax.swing.JLabel; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import javax.swing.JComboBox; +import java.awt.Insets; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JTextField; + +import mobimeta.EXTHRecord; + +public class NewRecordDialog extends JDialog implements ActionListener +{ + + /** + * + */ + private static final long serialVersionUID = 1L; + private final JPanel contentPanel = new JPanel(); + private JTextField tfValue; + private JComboBox typeCombo; + private EXTHAddRecordListener listener; + private JButton addButton; + private JButton cancelButton; + + + /** + * Create the dialog. + */ + public NewRecordDialog(JFrame parent, EXTHAddRecordListener listener) + { + super(parent, true); + + this.listener = listener; + String[] comboValues = new String[EXTHRecord.knownTypes.length]; + for (int i=0; i recordList = null; + + public EXTHHeader() + { + recordList = new LinkedList(); + } + + public EXTHHeader(List list) + { + setRecordList(list); + } + + public EXTHHeader(InputStream in) throws IOException + { + MobiCommon.logMessage("*** EXTHHeader ***"); + + StreamUtils.readByteArray(in, identifier); + if ((identifier[0] != 69) + || + (identifier[1] != 88) + || + (identifier[2] != 84) + || + (identifier[3] != 72)) + { + throw new IOException("Expected to find EXTH header identifier" + + " EXTH but got something else instead"); + } + + StreamUtils.readByteArray(in, headerLength); + + if (MobiCommon.debug) + { + MobiCommon.logMessage("EXTH header length: " + + StreamUtils.byteArrayToLong(headerLength)); + } + + StreamUtils.readByteArray(in, recordCount); + int count = StreamUtils.byteArrayToInt(recordCount); + MobiCommon.logMessage("EXTH record count: " + count); + + recordList = new LinkedList(); + for (int i=0; i getRecordList() + { + LinkedList list = new LinkedList(); + for (EXTHRecord rec : recordList) + { + list.add(rec.copy()); + } + + return list; + } + + public void setRecordList(List list) + { + recordList = new LinkedList(); + if (list != null) + { + for (EXTHRecord rec : list) + { + recordList.add(rec.copy()); + } + } + recomputeFields(); + } + + public void removeRecordsWithType(int type) + { + boolean changed = false; + for (EXTHRecord rec : recordList) + { + if (rec.getRecordType() == type) + { + recordList.remove(rec); + changed = true; + } + } + + if (changed) recomputeFields(); + } + + public boolean recordsWithTypeExist(int type) + { + for (EXTHRecord rec : recordList) + { + if (rec.getRecordType() == type) return true; + } + return false; + } + + public void setAllRecordsWithTypeToString(int type, + String s, + String encoding) + { + boolean changed = false; + for (EXTHRecord rec : recordList) + { + if (rec.getRecordType() == type) + { + rec.setData(s, encoding); + changed = true; + } + } + + if (changed) recomputeFields(); + } + + public void addRecord(int recType, String s, String encoding) + { + EXTHRecord rec = new EXTHRecord + (recType, StreamUtils.stringToByteArray(s, encoding)); + recordList.add(rec); + recomputeFields(); + } + + public void addRecord(int recType, byte[] buffer) + { + recordList.add(new EXTHRecord(recType, buffer)); + recomputeFields(); + } + + protected int dataSize() + { + int size = 0; + for (EXTHRecord rec : recordList) + { + size += rec.size(); + } + + return size; + } + + protected int paddingSize(int dataSize) + { + int paddingSize = dataSize % 4; + if (paddingSize != 0) paddingSize = 4 - paddingSize; + + return paddingSize; + } + + public void write(OutputStream out) throws IOException + { + out.write(identifier); + out.write(headerLength); + out.write(recordCount); + for (EXTHRecord rec : recordList) + { + rec.write(out); + } + int padding = paddingSize(dataSize()); + for (int i=0; i typeHash; + private static HashSet booleanTypesSet; + + private byte[] recordType = { 0, 0, 0, 0 }; + private byte[] recordLength = { 0, 0, 0, 0 }; + private byte[] recordData = null; + + static + { + typeHash = new HashMap(knownTypes.length); + for (int i=0; i(booleanTypes.length); + for (int i=0; i 0) + { + System.arraycopy(data, 0, recordData, 0, len); + } + } + + public EXTHRecord(InputStream in) throws IOException + { + MobiCommon.logMessage("*** EXTHRecord ***"); + + StreamUtils.readByteArray(in, recordType); + StreamUtils.readByteArray(in, recordLength); + + int len = StreamUtils.byteArrayToInt(recordLength); + if (len < 8) throw new IOException("Invalid EXTH record length"); + + recordData = new byte[len - 8]; + StreamUtils.readByteArray(in, recordData); + + if (MobiCommon.debug) + { + int recType = StreamUtils.byteArrayToInt(recordType); + System.out.print("EXTH record type: "); + switch (recType) + { + case 100: + MobiCommon.logMessage("author"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 101: + MobiCommon.logMessage("publisher"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 103: + MobiCommon.logMessage("description"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 104: + MobiCommon.logMessage("isbn"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 105: + MobiCommon.logMessage("subject"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 106: + MobiCommon.logMessage("publishingdate"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 109: + MobiCommon.logMessage("rights"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 113: + case 504: + MobiCommon.logMessage("asin"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 118: + MobiCommon.logMessage("retail price"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 119: + MobiCommon.logMessage("retail price currency"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 200: + MobiCommon.logMessage("dictionary short name"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + case 404: + MobiCommon.logMessage("text to speech"); + int ttsflag = StreamUtils.byteArrayToInt(recordData); + MobiCommon.logMessage((ttsflag == 0)?"enabled":"disabled"); + break; + case 501: + MobiCommon.logMessage("cdetype"); + MobiCommon.logMessage + (StreamUtils.byteArrayToString(recordData)); + break; + default: + MobiCommon.logMessage(Integer.toString(recType)); + } + } + } + + public int getRecordType() + { + return StreamUtils.byteArrayToInt(recordType); + } + + public byte[] getData() + { + return recordData; + } + + public int getDataLength() + { + return recordData.length; + } + + public int size() + { + return getDataLength() + 8; + } + + public void setData(String s, String encoding) + { + recordData = StreamUtils.stringToByteArray(s, encoding); + StreamUtils.intToByteArray(size(), recordLength); + } + + public void setData(int value) + { + if (recordData == null) + { + recordData = new byte[4]; + StreamUtils.intToByteArray(size(), recordLength); + } + + StreamUtils.intToByteArray(value, recordData); + } + + public void setData(boolean value) + { + if (recordData == null) + { + recordData = new byte[1]; + StreamUtils.intToByteArray(size(), recordLength); + } + + StreamUtils.intToByteArray(value?1:0, recordData); + } + + public EXTHRecord copy() + { + return new EXTHRecord(StreamUtils.byteArrayToInt(recordType), + recordData); + } + + public boolean isKnownType() + { + return isKnownType(StreamUtils.byteArrayToInt(recordType)); + } + + public String getTypeDescription() + { + return getDescriptionForType(StreamUtils.byteArrayToInt(recordType)); + } + + public void write(OutputStream out) throws IOException + { + if (MobiCommon.debug) + { + MobiCommon.logMessage("*** Write EXTHRecord ***"); + MobiCommon.logMessage(StreamUtils.dumpByteArray(recordType)); + MobiCommon.logMessage(StreamUtils.dumpByteArray(recordLength)); + MobiCommon.logMessage(StreamUtils.dumpByteArray(recordData)); + MobiCommon.logMessage("************************"); + } + out.write(recordType); + out.write(recordLength); + out.write(recordData); + } +} diff --git a/src/mobimeta/MobiCommon.java b/src/mobimeta/MobiCommon.java new file mode 100644 index 0000000..1dc977a --- /dev/null +++ b/src/mobimeta/MobiCommon.java @@ -0,0 +1,15 @@ +package mobimeta; + +public class MobiCommon +{ + public static boolean debug = false; + + // safe mode avoids changing the size of the mobi header + // + public static boolean safeMode = false; + + public static void logMessage(String message) + { + if (debug) System.out.println(message); + } +} diff --git a/src/mobimeta/MobiHeader.java b/src/mobimeta/MobiHeader.java new file mode 100644 index 0000000..d2f2767 --- /dev/null +++ b/src/mobimeta/MobiHeader.java @@ -0,0 +1,526 @@ +package mobimeta; + +import java.io.*; +import java.util.*; + +public class MobiHeader +{ + private byte[] compression = { 0, 0 }; + private byte[] unused0 = { 0, 0 }; + private byte[] textLength = { 0, 0, 0, 0 }; + private byte[] recordCount = { 0, 0 }; + private byte[] recordSize = { 0, 0 }; + private byte[] encryptionType = { 0, 0 }; + private byte[] unused1 = { 0, 0 }; + private byte[] identifier = { 0, 0, 0, 0 }; + private byte[] headerLength = { 0, 0, 0, 0 }; // from offset 0x10 + private byte[] mobiType = { 0, 0, 0, 0 }; + private byte[] textEncoding = { 0, 0, 0, 0 }; + private byte[] uniqueID = { 0, 0, 0, 0 }; + private byte[] fileVersion = { 0, 0, 0, 0 }; + private byte[] orthographicIndex = { 0, 0, 0, 0 }; + private byte[] inflectionIndex = { 0, 0, 0, 0 }; + private byte[] indexNames = { 0, 0, 0, 0 }; + private byte[] indexKeys = { 0, 0, 0, 0 }; + private byte[] extraIndex0 = { 0, 0, 0, 0 }; + private byte[] extraIndex1 = { 0, 0, 0, 0 }; + private byte[] extraIndex2 = { 0, 0, 0, 0 }; + private byte[] extraIndex3 = { 0, 0, 0, 0 }; + private byte[] extraIndex4 = { 0, 0, 0, 0 }; + private byte[] extraIndex5 = { 0, 0, 0, 0 }; + private byte[] firstNonBookIndex = { 0, 0, 0, 0 }; + private byte[] fullNameOffset = { 0, 0, 0, 0 }; + private byte[] fullNameLength = { 0, 0, 0, 0 }; + private byte[] locale = { 0, 0, 0, 0 }; + private byte[] inputLanguage = { 0, 0, 0, 0 }; + private byte[] outputLanguage = { 0, 0, 0, 0 }; + private byte[] minVersion = { 0, 0, 0, 0 }; + private byte[] firstImageIndex = { 0, 0, 0, 0 }; + private byte[] huffmanRecordOffset = { 0, 0, 0, 0 }; + private byte[] huffmanRecordCount = { 0, 0, 0, 0 }; + private byte[] huffmanTableOffset = { 0, 0, 0, 0 }; + private byte[] huffmanTableLength = { 0, 0, 0, 0 }; + private byte[] exthFlags = { 0, 0, 0, 0 }; + private byte[] restOfMobiHeader = null; + private EXTHHeader exthHeader = null; + private byte[] remainder = null; + // end of useful data + + + private byte[] fullName = null; + private String characterEncoding = null; + + public MobiHeader(InputStream in, long mobiHeaderSize) throws IOException + { + MobiCommon.logMessage("*** MobiHeader ***"); + MobiCommon.logMessage("compression"); + StreamUtils.readByteArray(in, compression); + StreamUtils.readByteArray(in, unused0); + StreamUtils.readByteArray(in, textLength); + StreamUtils.readByteArray(in, recordCount); + StreamUtils.readByteArray(in, recordSize); + MobiCommon.logMessage("encryptionType"); + StreamUtils.readByteArray(in, encryptionType); + StreamUtils.readByteArray(in, unused1); + + StreamUtils.readByteArray(in, identifier); + if (MobiCommon.debug) + { + MobiCommon.logMessage("identifier: " + + StreamUtils.byteArrayToString(identifier)); + } + if ((identifier[0] != 77) + || + (identifier[1] != 79) + || + (identifier[2] != 66) + || + (identifier[3] != 73)) + { + throw new IOException("Did not get expected MOBI identifier"); + } + + // this value will determine the size of restOfMobiHeader[] + // + StreamUtils.readByteArray(in, headerLength); + int headLen = StreamUtils.byteArrayToInt(headerLength); + restOfMobiHeader = new byte[headLen + 16 - 132]; + if (MobiCommon.debug) + { + MobiCommon.logMessage("headerLength: " + headLen); + } + + StreamUtils.readByteArray(in, mobiType); + if (MobiCommon.debug) + { + MobiCommon.logMessage("mobiType: " + + StreamUtils.byteArrayToInt(mobiType)); + } + + StreamUtils.readByteArray(in, textEncoding); + switch (StreamUtils.byteArrayToInt(textEncoding)) + { + case 1252: + characterEncoding = "Cp1252"; + break; + case 65001: + characterEncoding = "UTF-8"; + break; + default: + characterEncoding = null; + break; + } + MobiCommon.logMessage("text encoding: " + characterEncoding); + + StreamUtils.readByteArray(in, uniqueID); + StreamUtils.readByteArray(in, fileVersion); + StreamUtils.readByteArray(in, orthographicIndex); + StreamUtils.readByteArray(in, inflectionIndex); + StreamUtils.readByteArray(in, indexNames); + StreamUtils.readByteArray(in, indexKeys); + StreamUtils.readByteArray(in, extraIndex0); + StreamUtils.readByteArray(in, extraIndex1); + StreamUtils.readByteArray(in, extraIndex2); + StreamUtils.readByteArray(in, extraIndex3); + StreamUtils.readByteArray(in, extraIndex4); + StreamUtils.readByteArray(in, extraIndex5); + StreamUtils.readByteArray(in, firstNonBookIndex); + StreamUtils.readByteArray(in, fullNameOffset); + if (MobiCommon.debug) + { + MobiCommon.logMessage("full name offset: " + + StreamUtils.byteArrayToInt(fullNameOffset)); + } + + StreamUtils.readByteArray(in, fullNameLength); + int fullNameLen = StreamUtils.byteArrayToInt(fullNameLength); + MobiCommon.logMessage("full name length: " + fullNameLen); + StreamUtils.readByteArray(in, locale); + StreamUtils.readByteArray(in, inputLanguage); + StreamUtils.readByteArray(in, outputLanguage); + StreamUtils.readByteArray(in, minVersion); + StreamUtils.readByteArray(in, firstImageIndex); + StreamUtils.readByteArray(in, huffmanRecordOffset); + StreamUtils.readByteArray(in, huffmanRecordCount); + StreamUtils.readByteArray(in, huffmanTableOffset); + StreamUtils.readByteArray(in, huffmanTableLength); + StreamUtils.readByteArray(in, exthFlags); + if (MobiCommon.debug) + { + MobiCommon.logMessage("exthFlags: " + + StreamUtils.byteArrayToInt(exthFlags)); + } + boolean exthExists = ((StreamUtils.byteArrayToInt(exthFlags) & 0x40) + != 0); + MobiCommon.logMessage("exthExists: " + exthExists); + StreamUtils.readByteArray(in, restOfMobiHeader); + + if (exthExists) + { + exthHeader = new EXTHHeader(in); + } + + int currentOffset = 132 + restOfMobiHeader.length + exthHeaderSize(); + + remainder = new byte[(int)(mobiHeaderSize - currentOffset)]; + StreamUtils.readByteArray(in, remainder); + + int fullNameIndexInRemainder + = StreamUtils.byteArrayToInt(fullNameOffset) - currentOffset; + fullName = new byte[fullNameLen]; + MobiCommon.logMessage("fullNameIndexInRemainder: " + + fullNameIndexInRemainder); + MobiCommon.logMessage("fullNameLen: " + fullNameLen); + + if ((fullNameIndexInRemainder >= 0) + && + (fullNameIndexInRemainder < remainder.length) + && + ((fullNameIndexInRemainder + fullNameLen) <= remainder.length) + && + (fullNameLen > 0)) + { + System.arraycopy(remainder, + fullNameIndexInRemainder, + fullName, + 0, + fullNameLen); + } + if (MobiCommon.debug) + { + MobiCommon.logMessage("full name: " + + StreamUtils.byteArrayToString(fullName)); + } + } + + public String getCharacterEncoding() + { + return characterEncoding; + } + + public String getFullName() + { + return StreamUtils.byteArrayToString(fullName, characterEncoding); + } + + public void setFullName(String s) + { + byte[] fullBytes = StreamUtils.stringToByteArray(s, characterEncoding); + int len = fullBytes.length; + StreamUtils.intToByteArray(len, fullNameLength); + + // the string must be terminated by 2 null bytes + // then this must end in a 4-byte boundary + // + int padding = (len + 2) % 4; + if (padding != 0) padding = 4 - padding; + padding += 2; + + byte[] buffer = new byte[len + padding]; + System.arraycopy(fullBytes, 0, buffer, 0, len); + for (int i=len; i getEXTHRecords() + { + return (exthHeader == null) ? (new LinkedList()) + : exthHeader.getRecordList(); + } + + public void setEXTHRecords(List list) + { + int flag = StreamUtils.byteArrayToInt(exthFlags) & 0xffffbf; + if ((list == null) || (list.size() == 0)) + { + exthHeader = null; + StreamUtils.intToByteArray(flag, exthFlags); + } + else + { + if (exthHeader == null) + exthHeader = new EXTHHeader(list); + else + exthHeader.setRecordList(list); + + StreamUtils.intToByteArray(flag | 0x40, exthFlags); + } + } + + public void pack() + { + if (!MobiCommon.safeMode) + { + // dump existing remainder, set to fullName + remainder = new byte[fullName.length]; + System.arraycopy(fullName, 0, remainder, 0, remainder.length); + + // adjust fullNameOffset + StreamUtils.intToByteArray(132 + restOfMobiHeader.length + + exthHeaderSize(), fullNameOffset); + } + } + + public int size() + { + return 132 + restOfMobiHeader.length + exthHeaderSize() + remainder.length; + } + + public String getCompression() + { + int comp = StreamUtils.byteArrayToInt(compression); + switch (comp) + { + case 1: + return "None"; + case 2: + return "PalmDOC"; + case 17480: + return "HUFF/CDIC"; + default: + return "Unknown (" + comp + ")"; + } + } + + public long getTextLength() + { + return StreamUtils.byteArrayToLong(textLength); + } + + public int getRecordCount() + { + return StreamUtils.byteArrayToInt(recordCount); + } + + public int getRecordSize() + { + return StreamUtils.byteArrayToInt(recordSize); + } + + public String getEncryptionType() + { + int enc = StreamUtils.byteArrayToInt(encryptionType); + switch (enc) + { + case 0: return "None"; + case 1: return "Old Mobipocket"; + case 2: return "Mobipocket"; + default: return "Unknown (" + enc + ")"; + } + } + + public long getHeaderLength() + { + return StreamUtils.byteArrayToLong(headerLength); + } + + public String getMobiType() + { + long type = StreamUtils.byteArrayToLong(mobiType); + if (type == 2) + return "Mobipocket Book"; + else if (type == 3) + return "PalmDoc Book"; + else if (type == 4) + return "Audio"; + else if (type == 257) + return "News"; + else if (type == 258) + return "News Feed"; + else if (type == 259) + return "News Magazine"; + else if (type == 513) + return "PICS"; + else if (type == 514) + return "WORD"; + else if (type == 515) + return "XLS"; + else if (type == 516) + return "PPT"; + else if (type == 517) + return "TEXT"; + else if (type == 518) + return "HTML"; + else + return "Unknown (" + type + ")"; + } + + public long getUniqueID() + { + return StreamUtils.byteArrayToLong(uniqueID); + } + + public long getFileVersion() + { + return StreamUtils.byteArrayToLong(fileVersion); + } + + public long getOrthographicIndex() + { + return StreamUtils.byteArrayToLong(orthographicIndex); + } + + public long getInflectionIndex() + { + return StreamUtils.byteArrayToLong(inflectionIndex); + } + + public long getIndexNames() + { + return StreamUtils.byteArrayToLong(indexNames); + } + + public long getIndexKeys() + { + return StreamUtils.byteArrayToLong(indexKeys); + } + + public long getExtraIndex0() + { + return StreamUtils.byteArrayToLong(extraIndex0); + } + + public long getExtraIndex1() + { + return StreamUtils.byteArrayToLong(extraIndex1); + } + + public long getExtraIndex2() + { + return StreamUtils.byteArrayToLong(extraIndex2); + } + + public long getExtraIndex3() + { + return StreamUtils.byteArrayToLong(extraIndex3); + } + + public long getExtraIndex4() + { + return StreamUtils.byteArrayToLong(extraIndex4); + } + + public long getExtraIndex5() + { + return StreamUtils.byteArrayToLong(extraIndex5); + } + + public long getFirstNonBookIndex() + { + return StreamUtils.byteArrayToLong(firstNonBookIndex); + } + + public long getFullNameOffset() + { + return StreamUtils.byteArrayToLong(fullNameOffset); + } + + public long getFullNameLength() + { + return StreamUtils.byteArrayToLong(fullNameLength); + } + + public long getMinVersion() + { + return StreamUtils.byteArrayToLong(minVersion); + } + + public long getHuffmanRecordOffset() + { + return StreamUtils.byteArrayToLong(huffmanRecordOffset); + } + + public long getHuffmanRecordCount() + { + return StreamUtils.byteArrayToLong(huffmanRecordCount); + } + + public long getHuffmanTableOffset() + { + return StreamUtils.byteArrayToLong(huffmanTableOffset); + } + + public long getHuffmanTableLength() + { + return StreamUtils.byteArrayToLong(huffmanTableLength); + } + + private int exthHeaderSize() + { + return (exthHeader == null)?0:exthHeader.size(); + } + + public void write(OutputStream out) throws IOException + { + out.write(compression); + out.write(unused0); + out.write(textLength); + out.write(recordCount); + out.write(recordSize); + out.write(encryptionType); + out.write(unused1); + out.write(identifier); + out.write(headerLength); + out.write(mobiType); + out.write(textEncoding); + out.write(uniqueID); + out.write(fileVersion); + out.write(orthographicIndex); + out.write(inflectionIndex); + out.write(indexNames); + out.write(indexKeys); + out.write(extraIndex0); + out.write(extraIndex1); + out.write(extraIndex2); + out.write(extraIndex3); + out.write(extraIndex4); + out.write(extraIndex5); + out.write(firstNonBookIndex); + out.write(fullNameOffset); + out.write(fullNameLength); + out.write(locale); + out.write(inputLanguage); + out.write(outputLanguage); + out.write(minVersion); + out.write(firstImageIndex); + out.write(huffmanRecordOffset); + out.write(huffmanRecordCount); + out.write(huffmanTableOffset); + out.write(huffmanTableLength); + out.write(exthFlags); + out.write(restOfMobiHeader); + if (exthHeader != null) exthHeader.write(out); + out.write(remainder); + } +} diff --git a/src/mobimeta/MobiMeta.java b/src/mobimeta/MobiMeta.java new file mode 100644 index 0000000..92e06dc --- /dev/null +++ b/src/mobimeta/MobiMeta.java @@ -0,0 +1,292 @@ +package mobimeta; + +import java.io.*; +import java.util.*; + +public class MobiMeta +{ + public final static int BUFFER_SIZE = 4096; + + protected PDBHeader pdbHeader; + protected MobiHeader mobiHeader; + protected String characterEncoding; + protected List exthRecords; + private File inputFile; + + + public MobiMeta(File f) throws MobiMetaException + { + inputFile = f; + FileInputStream in = null; + try + { + in = new FileInputStream(f); + pdbHeader = new PDBHeader(in); + mobiHeader = new MobiHeader(in, pdbHeader.getMobiHeaderSize()); + exthRecords = mobiHeader.getEXTHRecords(); + characterEncoding = mobiHeader.getCharacterEncoding(); + } + catch (IOException e) + { + throw new MobiMetaException("Could not parse mobi file " + + f.getAbsolutePath() + + ": " + + e.getMessage()); + } + finally + { + if (in != null) try { in.close(); } catch (IOException e) {} + } + } + + public void saveToNewFile(File outputFile) throws MobiMetaException + { + saveToNewFile(outputFile, true); + } + + public void saveToNewFile(File outputFile, boolean packHeader) throws MobiMetaException + { + long readOffset = pdbHeader.getOffsetAfterMobiHeader(); + + if (!MobiCommon.safeMode && packHeader) + { + mobiHeader.pack(); + pdbHeader.adjustOffsetsAfterMobiHeader(mobiHeader.size()); + } + + FileInputStream in = null; + FileOutputStream out = null; + try + { + out = new FileOutputStream(outputFile); + pdbHeader.write(out); + mobiHeader.write(out); + + int bytesRead; + byte[] buffer = new byte[BUFFER_SIZE]; + in = new FileInputStream(inputFile); + in.skip(readOffset); + while ((bytesRead = in.read(buffer)) != -1) + { + out.write(buffer, 0, bytesRead); + } + } + catch (IOException e) + { + throw new MobiMetaException( + "Problems encountered while writing to " + + outputFile.getAbsolutePath() + ": " + + e.getMessage()); + } + finally + { + if (in != null) try { in.close(); } catch (IOException e) {} + if (out != null) try { out.close(); } catch (IOException e) {} + } + } + + public String getCharacterEncoding() + { + return mobiHeader.getCharacterEncoding(); + } + + public String getFullName() + { + return mobiHeader.getFullName(); + } + + public void setFullName(String s) + { + mobiHeader.setFullName(s); + } + + public List getEXTHRecords() + { + return exthRecords; + } + + public void setEXTHRecords() + { + mobiHeader.setEXTHRecords(exthRecords); + } + + public int getLocale() + { + return mobiHeader.getLocale(); + } + + public int getDictInput() + { + return mobiHeader.getInputLanguage(); + } + + public int getDictOutput() + { + return mobiHeader.getOutputLanguage(); + } + + public void setLanguages(int locale, int dictInput, int dictOutput) + { + mobiHeader.setLocale(locale); + mobiHeader.setInputLanguage(dictInput); + mobiHeader.setOutputLanguage(dictOutput); + } + + public String getMetaInfo() + { + StringBuffer sb = new StringBuffer(); + sb.append("PDB Header\r\n"); + sb.append("----------\r\n"); + sb.append("Name: "); + sb.append(pdbHeader.getName()); + sb.append("\r\n"); + String[] attributes = getPDBHeaderAttributes(); + if (attributes.length > 0) + { + sb.append("Attributes: "); + for (int i=0; i 0) sb.append(", "); + sb.append(attributes[i]); + } + sb.append("\r\n"); + } + sb.append("Version: "); + sb.append(pdbHeader.getVersion()); + sb.append("\r\n"); + sb.append("Creation Date: "); + sb.append(pdbHeader.getCreationDate()); + sb.append("\r\n"); + sb.append("Modification Date: "); + sb.append(pdbHeader.getModificationDate()); + sb.append("\r\n"); + sb.append("Last Backup Date: "); + sb.append(pdbHeader.getLastBackupDate()); + sb.append("\r\n"); + sb.append("Modification Number: "); + sb.append(pdbHeader.getModificationNumber()); + sb.append("\r\n"); + sb.append("App Info ID: "); + sb.append(pdbHeader.getAppInfoID()); + sb.append("\r\n"); + sb.append("Sort Info ID: "); + sb.append(pdbHeader.getSortInfoID()); + sb.append("\r\n"); + sb.append("Type: "); + sb.append(pdbHeader.getType()); + sb.append("\r\n"); + sb.append("Creator: "); + sb.append(pdbHeader.getCreator()); + sb.append("\r\n"); + sb.append("Unique ID Seed: "); + sb.append(pdbHeader.getUniqueIDSeed()); + sb.append("\r\n\r\n"); + + sb.append("PalmDOC Header\r\n"); + sb.append("--------------\r\n"); + sb.append("Compression: "); + sb.append(mobiHeader.getCompression()); + sb.append("\r\n"); + sb.append("Text Length: "); + sb.append(mobiHeader.getTextLength()); + sb.append("\r\n"); + sb.append("Record Count: "); + sb.append(mobiHeader.getRecordCount()); + sb.append("\r\n"); + sb.append("Record Size: "); + sb.append(mobiHeader.getRecordSize()); + sb.append("\r\n"); + sb.append("Encryption Type: "); + sb.append(mobiHeader.getEncryptionType()); + sb.append("\r\n\r\n"); + + sb.append("MOBI Header\r\n"); + sb.append("-----------\r\n"); + sb.append("Header Length: "); + sb.append(mobiHeader.getHeaderLength()); + sb.append("\r\n"); + sb.append("Mobi Type: "); + sb.append(mobiHeader.getMobiType()); + sb.append("\r\n"); + sb.append("Unique ID: "); + sb.append(mobiHeader.getUniqueID()); + sb.append("\r\n"); + sb.append("File Version: "); + sb.append(mobiHeader.getFileVersion()); + sb.append("\r\n"); + sb.append("Orthographic Index: "); + sb.append(mobiHeader.getOrthographicIndex()); + sb.append("\r\n"); + sb.append("Inflection Index: "); + sb.append(mobiHeader.getInflectionIndex()); + sb.append("\r\n"); + sb.append("Index Names: "); + sb.append(mobiHeader.getIndexNames()); + sb.append("\r\n"); + sb.append("Index Keys: "); + sb.append(mobiHeader.getIndexKeys()); + sb.append("\r\n"); + sb.append("Extra Index 0: "); + sb.append(mobiHeader.getExtraIndex0()); + sb.append("\r\n"); + sb.append("Extra Index 1: "); + sb.append(mobiHeader.getExtraIndex1()); + sb.append("\r\n"); + sb.append("Extra Index 2: "); + sb.append(mobiHeader.getExtraIndex2()); + sb.append("\r\n"); + sb.append("Extra Index 3: "); + sb.append(mobiHeader.getExtraIndex3()); + sb.append("\r\n"); + sb.append("Extra Index 4: "); + sb.append(mobiHeader.getExtraIndex4()); + sb.append("\r\n"); + sb.append("Extra Index 5: "); + sb.append(mobiHeader.getExtraIndex5()); + sb.append("\r\n"); + sb.append("First Non-Book Index: "); + sb.append(mobiHeader.getFirstNonBookIndex()); + sb.append("\r\n"); + sb.append("Full Name Offset: "); + sb.append(mobiHeader.getFullNameOffset()); + sb.append("\r\n"); + sb.append("Full Name Length: "); + sb.append(mobiHeader.getFullNameLength()); + sb.append("\r\n"); + sb.append("Min Version: "); + sb.append(mobiHeader.getMinVersion()); + sb.append("\r\n"); + sb.append("Huffman Record Offset: "); + sb.append(mobiHeader.getHuffmanRecordOffset()); + sb.append("\r\n"); + sb.append("Huffman Record Count: "); + sb.append(mobiHeader.getHuffmanRecordCount()); + sb.append("\r\n"); + sb.append("Huffman Table Offset: "); + sb.append(mobiHeader.getHuffmanTableOffset()); + sb.append("\r\n"); + sb.append("Huffman Table Length: "); + sb.append(mobiHeader.getHuffmanTableLength()); + sb.append("\r\n"); + + return sb.toString(); + } + + private String[] getPDBHeaderAttributes() + { + LinkedList list = new LinkedList(); + int attr = pdbHeader.getAttributes(); + if ((attr & 0x02) != 0) list.add("Read-Only"); + if ((attr & 0x04) != 0) list.add("Dirty AppInfoArea"); + if ((attr & 0x08) != 0) list.add("Backup This Database"); + if ((attr & 0x10) != 0) list.add("OK To Install Newer Over Existing Copy"); + if ((attr & 0x20) != 0) list.add("Force The PalmPilot To Reset After This Database Is Installed"); + if ((attr & 0x40) != 0) list.add("Don't Allow Copy Of File To Be Beamed To Other Pilot"); + + String[] ret = new String[list.size()]; + int index = 0; + for (String s : list) ret[index++] = s; + + return ret; + } +} diff --git a/src/mobimeta/MobiMetaException.java b/src/mobimeta/MobiMetaException.java new file mode 100644 index 0000000..2e1b777 --- /dev/null +++ b/src/mobimeta/MobiMetaException.java @@ -0,0 +1,14 @@ +package mobimeta; + +public class MobiMetaException extends Exception +{ + /** + * + */ + private static final long serialVersionUID = 1L; + + public MobiMetaException(String message) + { + super(message); + } +} diff --git a/src/mobimeta/PDBHeader.java b/src/mobimeta/PDBHeader.java new file mode 100644 index 0000000..e810727 --- /dev/null +++ b/src/mobimeta/PDBHeader.java @@ -0,0 +1,164 @@ +package mobimeta; + +import java.io.*; +import java.util.*; + +public class PDBHeader +{ + private byte[] name = { 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 }; + private byte[] attributes = { 0, 0 }; + private byte[] version = { 0, 0 }; + private byte[] creationDate = { 0, 0, 0, 0 }; + private byte[] modificationDate = { 0, 0, 0, 0 }; + private byte[] lastBackupDate = { 0, 0, 0, 0 }; + private byte[] modificationNumber = { 0, 0, 0, 0 }; + private byte[] appInfoID = { 0, 0, 0, 0 }; + private byte[] sortInfoID = { 0, 0, 0, 0 }; + private byte[] type = { 0, 0, 0, 0 }; + private byte[] creator = { 0, 0, 0, 0 }; + private byte[] uniqueIDSeed = { 0, 0, 0, 0 }; + private byte[] nextRecordListID = { 0, 0, 0, 0 }; + private byte[] numRecords = { 0, 0 }; + private List recordInfoList; + private byte[] gapToData = { 0, 0 }; + + public PDBHeader(InputStream in) + throws IOException + { + MobiCommon.logMessage("*** PDBHeader ***"); + StreamUtils.readByteArray(in, name); + StreamUtils.readByteArray(in, attributes); + StreamUtils.readByteArray(in, version); + StreamUtils.readByteArray(in, creationDate); + StreamUtils.readByteArray(in, modificationDate); + StreamUtils.readByteArray(in, lastBackupDate); + StreamUtils.readByteArray(in, modificationNumber); + StreamUtils.readByteArray(in, appInfoID); + StreamUtils.readByteArray(in, sortInfoID); + StreamUtils.readByteArray(in, type); + StreamUtils.readByteArray(in, creator); + StreamUtils.readByteArray(in, uniqueIDSeed); + StreamUtils.readByteArray(in, nextRecordListID); + StreamUtils.readByteArray(in, numRecords); + + int recordCount = StreamUtils.byteArrayToInt(numRecords); + MobiCommon.logMessage("numRecords: " + recordCount); + recordInfoList = new LinkedList(); + for (int i=0; i 1) ? (recordInfoList.get(1) + .getRecordDataOffset() - recordInfoList.get(0) + .getRecordDataOffset()) : 0; + } + + public long getOffsetAfterMobiHeader() + { + return (recordInfoList.size() > 1) ? recordInfoList.get(1) + .getRecordDataOffset() : 0; + } + + public void adjustOffsetsAfterMobiHeader(int newMobiHeaderSize) + { + if (recordInfoList.size() < 2) return; + + int delta = (int)(newMobiHeaderSize - getMobiHeaderSize()); + int len = recordInfoList.size(); + for (int i=1; i 0) + { + int bytesRead = in.read(buffer, offset, bytesLeft); + if (bytesRead == -1) + throw new IOException("Supposed to read a " + + len + + " byte C string, but could not"); + offset += bytesRead; + bytesLeft -= bytesRead; + } + + String s = byteArrayToString(buffer); + MobiCommon.logMessage("readCString: " + s); + return s; + } + + public static byte readByte(InputStream in) + throws IOException + { + int b = in.read(); + if (b == -1) + throw new IOException("Supposed to read a byte, but could not"); + MobiCommon.logMessage("readByte: " + b); + return (byte)(b & 0xff); + } + + public static void readByteArray(InputStream in, byte[] buffer) + throws IOException + { + int len = buffer.length; + int bytesLeft = len; + int offset = 0; + + while (bytesLeft > 0) + { + int bytesRead = in.read(buffer, offset, bytesLeft); + if (bytesRead == -1) + throw new IOException("Supposed to read a " + + len + + " byte array, but could not"); + offset += bytesRead; + bytesLeft -= bytesRead; + } + + if (MobiCommon.debug) + { + MobiCommon.logMessage(dumpByteArray(buffer)); + } + } + + public static String byteArrayToString(byte[] buffer) + { + return byteArrayToString(buffer, null); + } + + public static String byteArrayToString(byte[] buffer, String encoding) + { + int len = buffer.length; + int zeroIndex = -1; + for (int i=0; i=0; i--) + { + dest[i] = (byte)(value & 0xff); + value = value >> 8; + } + } + + public static void longToByteArray(long value, byte[] dest) + { + int lastIndex = dest.length - 1; + for (int i=lastIndex; i >=0; i--) + { + dest[i] = (byte)(value & 0xff); + value = value >> 8; + } + } + + public static byte[] stringToByteArray(String s) + { + return stringToByteArray(s, null); + } + + public static byte[] stringToByteArray(String s, String encoding) + { + if (encoding != null) + { + try + { + return s.getBytes(encoding); + } + catch (UnsupportedEncodingException e) + { + // let if fall through to use the default character encoding + } + } + + return s.getBytes(); + } + + public static String dumpByteArray(byte[] buffer) + { + StringBuffer sb = new StringBuffer(); + sb.append("{ "); + int len = buffer.length; + for (int i=0; i 0) sb.append(", "); + sb.append(buffer[i] & 0xff); + } + sb.append(" }"); + return sb.toString(); + } +}