diff --git a/liquibase-core/src/main/java/liquibase/parser/core/yaml/YamlChangeLogParser.java b/liquibase-core/src/main/java/liquibase/parser/core/yaml/YamlChangeLogParser.java index b5411d8bfbb..c131fa6de20 100644 --- a/liquibase-core/src/main/java/liquibase/parser/core/yaml/YamlChangeLogParser.java +++ b/liquibase-core/src/main/java/liquibase/parser/core/yaml/YamlChangeLogParser.java @@ -13,8 +13,10 @@ import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.*; public class YamlChangeLogParser extends YamlParser implements ChangeLogParser { @@ -83,11 +85,78 @@ public DatabaseChangeLog parse(String physicalChangeLogLocation, ChangeLogParame throw new ChangeLogParseException("Error parsing "+physicalChangeLogLocation, e); } } + + /** + * A simple "interceptor" InputStream to ensure that the YAML stream doesn't contain two "databaseChangeLog" tags, + * which would otherwise cause silent omissions if something is copy/pasted wrongly into the file. + */ + static class DoubleChangelogCheckerInputStream extends FilterInputStream { + + byte[] databaseChangeLog = "databaseChangeLog".getBytes(StandardCharsets.UTF_8); + int scanPos; + boolean databaseChangeLogSeen; + + DoubleChangelogCheckerInputStream(InputStream in) { + super(in); + scanPos = 0; + databaseChangeLogSeen = false; + } + + private void processByte(int nextByte) { + // scanPos will be -1 if we aren't in a position where "databaseChangeLog" would cause a problem + // i.e. the beginning of a file or of a line + if (nextByte == 10) { + // a new line means we should expect to see it again + scanPos = 0; + } else if (scanPos > -1 && nextByte == databaseChangeLog[scanPos]) { + scanPos++; + if (scanPos == databaseChangeLog.length) { + if (databaseChangeLogSeen) { + throw new IllegalStateException("Two 'databaseChangeLog' tags found in a single file!"); + } else { + databaseChangeLogSeen = true; + scanPos = -1; + } + } + } else { + // no point in scanning since the string was not found + scanPos = -1; + } + } + + @Override + public int read() throws IOException { + int nextByte = super.read(); + processByte(nextByte); + return nextByte; + } + + @Override + public int read(byte[] b) throws IOException { + int numBytes = super.read(b); + + for (int i=0; i