2
0

Fix file import & sane format migration.

This commit is contained in:
Maarten Billemont 2018-10-13 16:44:37 -04:00
parent 46fe919476
commit ff17a1d637
8 changed files with 131 additions and 68 deletions

@ -1 +1 @@
Subproject commit bc737d41fa15c9e4d05e7f73c25995da67611e52 Subproject commit 3d04d775e01ab1a437bb42166e92662ffffeedd4

View File

@ -3,7 +3,7 @@ package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.*; import com.google.common.base.*;
import com.google.common.collect.*; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
@ -407,6 +407,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Res.job( () -> { Res.job( () -> {
try { try {
user.authenticate( masterPassword ); user.authenticate( masterPassword );
if (user instanceof MPFileUser)
((MPFileUser) user).migrateTo( MPMarshalFormat.DEFAULT );
} }
catch (final MPIncorrectMasterPasswordException e) { catch (final MPIncorrectMasterPasswordException e) {
logger.wrn( e, "During user authentication for: %s", user ); logger.wrn( e, "During user authentication for: %s", user );

View File

@ -24,7 +24,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.Instant; import org.joda.time.Instant;
@ -41,7 +40,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Nullable @Nullable
private byte[] keyID; private byte[] keyID;
private File path; private File file;
private MPMarshalFormat format; private MPMarshalFormat format;
private MPMarshaller.ContentMode contentMode; private MPMarshaller.ContentMode contentMode;
@ -54,33 +53,37 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public static MPFileUser load(final File file) public static MPFileUser load(final File file)
throws IOException, MPMarshalException { throws IOException, MPMarshalException {
for (final MPMarshalFormat format : MPMarshalFormat.values()) for (final MPMarshalFormat format : MPMarshalFormat.values())
if (file.getName().endsWith( format.fileSuffix() )) if (format.matches(file))
return format.unmarshaller().readUser( file ); return format.unmarshaller().readUser( file );
return null; return null;
} }
public MPFileUser(final String fullName, final File path) { public MPFileUser(final String fullName, final File location) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path ); this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), location );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) { public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File location) {
this( fullName, keyID, algorithm, 0, null, new Instant(), false, this( fullName, keyID, algorithm, 0, null, new Instant(), false,
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path ); MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, location );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar, public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar,
@Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) { final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File location) {
super( avatar, fullName, algorithm ); super( avatar, fullName, algorithm );
this.keyID = (keyID != null)? keyID.clone(): null; this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type(); this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
this.hidePasswords = hidePasswords; this.hidePasswords = hidePasswords;
this.path = path;
this.format = format; this.format = format;
this.contentMode = contentMode; this.contentMode = contentMode;
if (location.isDirectory())
this.file = new File( location, getFullName() + getFormat().fileSuffix() );
else
this.file = location;
} }
@Nullable @Nullable
@ -89,10 +92,6 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return (keyID == null)? null: keyID.clone(); return (keyID == null)? null: keyID.clone();
} }
public void setPath(final File path) {
this.path = path;
}
@Override @Override
public void setAlgorithm(final MPAlgorithm algorithm) { public void setAlgorithm(final MPAlgorithm algorithm) {
if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) { if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) {
@ -117,20 +116,12 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return format; return format;
} }
public void setFormat(final MPMarshalFormat format) {
if (Objects.equals( this.format, format ))
return;
this.format = format;
setChanged();
}
public MPMarshaller.ContentMode getContentMode() { public MPMarshaller.ContentMode getContentMode() {
return contentMode; return contentMode;
} }
public void setContentMode(final MPMarshaller.ContentMode contentMode) { public void setContentMode(final MPMarshaller.ContentMode contentMode) {
if (Objects.equals( this.contentMode, contentMode )) if (this.contentMode == contentMode)
return; return;
this.contentMode = contentMode; this.contentMode = contentMode;
@ -143,7 +134,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
public void setDefaultType(final MPResultType defaultType) { public void setDefaultType(final MPResultType defaultType) {
if (Objects.equals( this.defaultType, defaultType )) if (this.defaultType == defaultType)
return; return;
this.defaultType = defaultType; this.defaultType = defaultType;
@ -180,7 +171,47 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
public File getFile() { public File getFile() {
return new File( path, getFullName() + getFormat().fileSuffix() ); return file;
}
public void migrateTo(final MPMarshalFormat format) {
if (this.format == format)
return;
migrateTo( file.getParentFile(), format );
}
public void migrateTo(final File path) {
migrateTo( path, format );
}
/**
* Move the file for this user to the given path using a standard user-derived filename (ie. {@code [full name].[format suffix]})
*
* The user's old file is either moved to the new or deleted. If the user's file was already at the destination, it doesn't change.
* If a file already exists at the destination, it is overwritten.
*/
public void migrateTo(final File path, final MPMarshalFormat newFormat) {
MPMarshalFormat oldFormat = format;
File oldFile = file, newFile = new File( path, getFullName() + newFormat.fileSuffix() );
// If the format hasn't changed, migrate by moving the file: the contents doesn't need to change.
if ((oldFormat == newFormat) && !oldFile.equals( newFile ) && oldFile.exists())
if (!oldFile.renameTo( newFile ))
logger.wrn( "Couldn't move %s to %s for migration.", oldFile, newFile );
this.format = newFormat;
this.file = newFile;
// If the format has changed, save the new format into the new file and delete the old file. Revert if the user cannot be saved.
if ((oldFormat != newFormat) && !oldFile.equals( newFile ))
if (save()) {
if (oldFile.exists() && !oldFile.delete())
logger.wrn( "Couldn't delete %s after migration.", oldFile );
} else {
this.format = oldFormat;
this.file = oldFile;
}
} }
@Override @Override
@ -201,10 +232,16 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
} }
public void save() { /**
* @return {@code false} if the user is not fully loaded (complete), authenticated, or an issue prevented the marshalling.
*/
public boolean save() {
if (!isComplete())
return false;
try { try {
if (isComplete())
getFormat().marshaller().marshall( this ); getFormat().marshaller().marshall( this );
return true;
} }
catch (final MPKeyUnavailableException e) { catch (final MPKeyUnavailableException e) {
logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
@ -212,6 +249,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
catch (final IOException | MPMarshalException | MPAlgorithmException e) { catch (final IOException | MPMarshalException | MPAlgorithmException e) {
logger.err( e, "Unable to write out changes for user: %s", this ); logger.err( e, "Unable to write out changes for user: %s", this );
} }
return false;
} }
@Override @Override

View File

@ -78,14 +78,13 @@ public class MPFileUserManager {
return; return;
} }
for (final MPMarshalFormat format : MPMarshalFormat.values())
for (final File file : pathFiles) for (final File file : pathFiles)
if (format.matches( file ))
try { try {
MPFileUser user = MPFileUser.load( file ); MPFileUser user = MPFileUser.load( file );
if (user != null) { if (user != null)
MPFileUser previousUser = userByName.put( user.getFullName(), user ); add( user );
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
userByName.put( previousUser.getFullName(), previousUser );
}
} }
catch (final IOException | MPMarshalException e) { catch (final IOException | MPMarshalException e) {
logger.err( e, "Couldn't read user from: %s", file ); logger.err( e, "Couldn't read user from: %s", file );
@ -99,12 +98,19 @@ public class MPFileUserManager {
} }
public MPFileUser add(final MPFileUser user) { public MPFileUser add(final MPFileUser user) {
user.setPath( getPath() ); // We migrate in two steps to allow the first to complete even if the user is not in the right state to complete the latter.
user.save(); user.migrateTo( getPath() );
user.migrateTo( MPMarshalFormat.DEFAULT );
MPFileUser oldUser = userByName.put( user.getFullName(), user ); MPFileUser oldUser = userByName.put( user.getFullName(), user );
if (oldUser != null) if (oldUser != null) {
oldUser.invalidate(); oldUser.invalidate();
// Delete old user, it is replaced by the new one.
if (!oldUser.getFile().equals( user.getFile() ) && oldUser.getFile().exists())
if (!oldUser.getFile().delete())
logger.err( "Couldn't delete file: %s, after replacing with: %s", oldUser.getFile(), user.getFile() );
}
fireUpdated(); fireUpdated();
return user; return user;

View File

@ -25,8 +25,8 @@ import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPModelConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPModelConstants;
import java.io.*; import java.io.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -56,6 +56,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
int mpVersion = 0, avatar = 0; int mpVersion = 0, avatar = 0;
boolean clearContent = false, headerStarted = false; boolean clearContent = false, headerStarted = false;
MPResultType defaultType = null; MPResultType defaultType = null;
Instant date = null;
//noinspection HardcodedLineSeparator //noinspection HardcodedLineSeparator
for (final String line : CharStreams.readLines( reader )) for (final String line : CharStreams.readLines( reader ))
@ -66,10 +67,11 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
headerStarted = true; headerStarted = true;
else if ((fullName != null) && (keyID != null)) else if ((fullName != null) && (keyID != null))
// Ends the header. // Ends the header.
return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), return new MPFileUser(
avatar, defaultType, new Instant( 0 ), false, fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), avatar, defaultType,
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED, date, false, clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
MPMarshalFormat.Flat, file.getParentFile() ); MPMarshalFormat.Flat, file
);
} }
// Comment. // Comment.
@ -91,6 +93,8 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
clearContent = "visible".equalsIgnoreCase( value ); clearContent = "visible".equalsIgnoreCase( value );
else if ("Default Type".equalsIgnoreCase( name )) else if ("Default Type".equalsIgnoreCase( name ))
defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) ); defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) );
else if ("Date".equalsIgnoreCase( name ))
date = MPModelConstants.dateTimeFormatter.parseDateTime( value ).toInstant();
} }
} }
} }
@ -150,8 +154,9 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
MPFileSite site; MPFileSite site;
switch (importFormat) { switch (importFormat) {
case 0: case 0:
site = new MPFileSite( user, // site = new MPFileSite(
siteMatcher.group( 5 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( user, siteMatcher.group( 5 ),
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
user.getAlgorithm().mpw_default_counter(), user.getAlgorithm().mpw_default_counter(),
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
@ -163,14 +168,17 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
break; break;
case 1: case 1:
site = new MPFileSite( user, // site = new MPFileSite(
siteMatcher.group( 7 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( user, siteMatcher.group( 7 ),
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
UnsignedInteger.valueOf( colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ), UnsignedInteger.valueOf(
colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ),
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
clearContent? null: siteMatcher.group( 8 ), clearContent? null: siteMatcher.group( 8 ),
MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null, MPResultType.GeneratedName,
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), clearContent? null: siteMatcher.group( 6 ),
null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
if (clearContent) { if (clearContent) {
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) ); site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );

View File

@ -144,7 +144,7 @@ public class MPJSONFile extends MPJSONAnyObject {
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
(user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), (user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
user.hide_passwords, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE, user.hide_passwords, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
MPMarshalFormat.JSON, file.getParentFile() MPMarshalFormat.JSON, file
); );
} }

View File

@ -18,6 +18,9 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import java.io.File;
/** /**
* @author lhunath, 2017-09-20 * @author lhunath, 2017-09-20
* *
@ -72,4 +75,8 @@ public enum MPMarshalFormat {
@SuppressWarnings("MethodReturnAlwaysConstant") @SuppressWarnings("MethodReturnAlwaysConstant")
public abstract String fileSuffix(); public abstract String fileSuffix();
public boolean matches(final File file) {
return file.getName().endsWith( fileSuffix() );
}
} }

@ -1 +1 @@
Subproject commit d8d510b6be2e2136a040624b8d0ed7590b6e7530 Subproject commit ebd2c741fd11171eefa081792f9b14d6b2bac356