[MSHADE-391] Do not write modified class files for no-op relocations
diff --git a/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/pom.xml b/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/pom.xml
new file mode 100644
index 0000000..4b7a670
--- /dev/null
+++ b/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<project>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.maven.its.shade.csj</groupId>
+    <artifactId>mshade-391</artifactId>
+    <version>1.0</version>
+    <url>https://issues.apache.org/jira/browse/MSHADE-391</url>
+    <description>
+        ASM-processed classes should not be written to the shaded JAR if they have not been relocated and do not refer
+        to relocated classes either. In that case, the user expects to retain the original classes.
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <!-- Replace the version by 3.2.4 or something older in order to see it fail -->
+                <version>@project.version@</version>
+                <executions>
+                    <execution>
+                        <id>shade</id>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <relocations>
+                                <relocation>
+                                    <pattern>org.apache.commons.io</pattern>
+                                    <shadedPattern>org.shaded.commons.io</shadedPattern>
+                                </relocation>
+                            </relocations>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/**</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/verify.groovy b/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/verify.groovy
new file mode 100644
index 0000000..4651a69
--- /dev/null
+++ b/src/it/projects/MSHADE-391_noRelocationKeepOriginalClasses/verify.groovy
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.security.MessageDigest
+import java.util.jar.JarFile
+
+def originalChecksumsMD5 = [
+    'ArrayUtils.class'                                    : '4644ce91f5dbf1172f3bc2eafa1220bc',
+    'BitField.class'                                      : 'ec5d367938cb9f59912fafbe708112fa',
+    'BooleanUtils.class'                                  : 'c321cc00995b4e81162cec9366406817',
+    'builder/CompareToBuilder.class'                      : '70c085203ca3506fb0fbd1eed8de9ffe',
+    'builder/EqualsBuilder.class'                         : 'e538ada29bf74d053b4174c6d0338c29',
+    'builder/HashCodeBuilder.class'                       : '76e129cfdadefc856588351c830570e6',
+    'builder/IDKey.class'                                 : 'fa08336106e492b5ac9f9901bd7ec690',
+    'builder/ReflectionToStringBuilder.class'             : '5a30e353b9f1bd0c9d8ed547c3ef576d',
+    'builder/StandardToStringStyle.class'                 : 'bd26865d3f4f0e3a0f24efe00897f4f3',
+    'builder/ToStringBuilder.class'                       : '8493114752dd3552e21eba63343e6558',
+    'builder/ToStringStyle$DefaultToStringStyle.class'    : 'e4d42ea42ea43ad971fe5d33624063a6',
+    'builder/ToStringStyle$MultiLineToStringStyle.class'  : '3f2306c0f664229419dad8bfcfb0142d',
+    'builder/ToStringStyle$NoFieldNameToStringStyle.class': '9ed4e831c533c0fc98f3583c1e6fda31',
+    'builder/ToStringStyle$ShortPrefixToStringStyle.class': '4aa105e83bcd37a06f6d598a9c59d91c',
+    'builder/ToStringStyle$SimpleToStringStyle.class'     : 'c156f9816faa3da7b664df81fe811b50',
+    'builder/ToStringStyle.class'                         : '4088eef1b6a33ff2b6483b2f79461364',
+    'CharEncoding.class'                                  : '68b058ceea276d04247aff21ef7dbb63',
+    'CharRange$1.class'                                   : 'b3034e9540791bc9ce371e75d0ba8785',
+    'CharRange$CharacterIterator.class'                   : 'fdb6df8bbf32f88f88749ade8552d081',
+    'CharRange.class'                                     : 'a6b95e2ed34c8e46a9889568e56bacb8',
+    'CharSet.class'                                       : '4e0df369374e877afefc76153797cffd',
+    'CharSetUtils.class'                                  : '94a3d9c03ce37f671e5bdbb6605fe18b',
+    'CharUtils.class'                                     : '5706b8e0802f818efac9e5c6c203d428',
+    'ClassUtils.class'                                    : '09bc083bfe4f11d562d32c14b88b5a8e',
+    'Entities$ArrayEntityMap.class'                       : 'ff3a4211ef9b3c463eb79c1d04ad8d25',
+    'Entities$BinaryEntityMap.class'                      : '12b7fdc0621bea444f687950466fd89b',
+    'Entities$EntityMap.class'                            : '8e35cd73a7f6d80f45b328c228bcbc7d',
+    'Entities$HashEntityMap.class'                        : 'e8ab521f1a3fdca02c7e44daa33f7688',
+    'Entities$LookupEntityMap.class'                      : '1e59392ae388c8da917881f9f8b4998d',
+    'Entities$MapIntMap.class'                            : 'ce32c8d2649bbf479e7c511d110bf9c2',
+    'Entities$PrimitiveEntityMap.class'                   : '110a8762a727788dc038d47385a55eaf',
+    'Entities$TreeEntityMap.class'                        : '2a51fb240cd9cc51dbf249ebc20c5639',
+    'Entities.class'                                      : '8d0f106f7591b55d0b57927d0fcce871',
+    'enum/Enum$Entry.class'                               : 'bb9c0b1ecfcd16e873d180ce20129e9e',
+    'enum/Enum.class'                                     : 'fda3012c2bc97b7d0425057b8d98ca30',
+    'enum/EnumUtils.class'                                : 'd9d966cd69bef5a14d9c514a3aec8a1d',
+    'enum/ValuedEnum.class'                               : '6196dd12bd0b6debcef93f78cae0f094',
+    'enums/Enum$Entry.class'                              : 'a625be33cd61f7e00da41d868ac5fbde',
+    'enums/Enum.class'                                    : '5769c6367611bdeaab3536c979e931ec',
+    'enums/EnumUtils.class'                               : '5c4c227735393bcd5d550172afe0d1e7',
+    'enums/ValuedEnum.class'                              : '7cc89eafa3f01eca7f989e4a95880484',
+    'exception/CloneFailedException.class'                : 'a1d1fe5140c39f1d9c13b9716df320e0',
+    'exception/ExceptionUtils.class'                      : 'bfbbc6b82e778a91c9ac885a582aa87a',
+    'exception/Nestable.class'                            : 'cb37cc4f6aa95ddc1e08e24dfdb1faeb',
+    'exception/NestableDelegate.class'                    : '72610ad6e1acd693a73bd169c4c2fe66',
+    'exception/NestableError.class'                       : 'e9dec41924015b2a37fcc724994601d7',
+    'exception/NestableException.class'                   : 'f88cb396c859f785b13272236dd2d14a',
+    'exception/NestableRuntimeException.class'            : 'b3712109e98e7dfe916c028944be71a2',
+    'IllegalClassException.class'                         : '592efb0cd4c69c78890f51e5aefd5323',
+    'IncompleteArgumentException.class'                   : 'c65def57686f1433fec9de1a53d7e968',
+    'IntHashMap$Entry.class'                              : '312431259a720c5cc59919164e1f6f94',
+    'IntHashMap.class'                                    : '09e0c9e37e06edc13912e9179d123df2',
+    'LocaleUtils.class'                                   : 'ba8b698331343045e4714ab276d84df3',
+    'math/DoubleRange.class'                              : 'ae74dc56efa1d6608a34cd87326775df',
+    'math/FloatRange.class'                               : 'ca15b57ff629ad21c135daa3af4ae748',
+    'math/Fraction.class'                                 : '966d7dba648d56d40415f1d76eb61f22',
+    'math/IEEE754rUtils.class'                            : '904f0342faa83dc32aa3ce09b570d6e6',
+    'math/IntRange.class'                                 : '1a08884805a90e85a4af5f38f5c1d3ae',
+    'math/JVMRandom.class'                                : '1c38d4c2b8dcd9feee7ca4affc945ef9',
+    'math/LongRange.class'                                : 'ebc6dbf88a701e592764d635234eeec4',
+    'math/NumberRange.class'                              : '78de0d669bc79bba406be19c5fdd0abb',
+    'math/NumberUtils.class'                              : '5ae54b27c20fa65144d64c65449aaa89',
+    'math/RandomUtils.class'                              : '40155ad57c6f32460e0f32f3db22b173',
+    'math/Range.class'                                    : 'dab8235e8a5cc7d9b9103f416478f39a',
+    'mutable/Mutable.class'                               : '0c3515bc904e276c781486c67ec34e71',
+    'mutable/MutableBoolean.class'                        : '1f5979176185bf9638ca60d15526b45e',
+    'mutable/MutableByte.class'                           : 'd458d842df3c178069a7969fc9f6af38',
+    'mutable/MutableDouble.class'                         : '1c32ae0b6cdfc00d48280164a6ef8e9b',
+    'mutable/MutableFloat.class'                          : 'f5a58dc62a9f95081f678db598f9513d',
+    'mutable/MutableInt.class'                            : '2350cc67b99f6427d404ea7233d94664',
+    'mutable/MutableLong.class'                           : '7459b453c34cfe6beeb877bcc5fc4e81',
+    'mutable/MutableObject.class'                         : 'f16ed36d34163fd6219c59fffdc02760',
+    'mutable/MutableShort.class'                          : 'e3192bee99861e409c941bc2a5be9add',
+    'NotImplementedException.class'                       : '45f4bfa15f637b7ac8fba0ea5af9a981',
+    'NullArgumentException.class'                         : '206902af1b00283b40b258b28385f63b',
+    'NumberRange.class'                                   : '54fbe10934f95117f8d3f0c0546b56c7',
+    'NumberUtils.class'                                   : '2de64e30b07a36e11f095f3146f16aa0',
+    'ObjectUtils$Null.class'                              : '1138f88d2527f63458eaa56c959f818c',
+    'ObjectUtils.class'                                   : '40483af3c9a0cdcb5ee0df43c1fa3ae8',
+    'RandomStringUtils.class'                             : '1a4f147bab572b579db352d06979ce0a',
+    'reflect/ConstructorUtils.class'                      : '96b06ea51ec96064b7d881975dffc8bb',
+    'reflect/FieldUtils.class'                            : 'f37be95c51f707ea1dd965d02f20f540',
+    'reflect/MemberUtils.class'                           : '9fe7b0a7c664458058668c5400a2139b',
+    'reflect/MethodUtils.class'                           : '4d7781bf651d8a1f750ba75f628f45e4',
+    'SerializationException.class'                        : '7eaeab8f24eeed40e089d4abeffc3ad4',
+    'SerializationUtils.class'                            : '32062839391b9896caa63067a4040a6c',
+    'StringEscapeUtils.class'                             : 'c1fe975a05f7d14823d3dbce29c23bb2',
+    'StringUtils.class'                                   : 'bb4c964dd2c5057d3a8f282ea381c05d',
+    'SystemUtils.class'                                   : '74492426bb3b60ea1e3c3816b955f40f',
+    'text/CompositeFormat.class'                          : 'dd2b0fc2a4784eb07754f8271d758ba5',
+    'text/ExtendedMessageFormat.class'                    : 'eb32c6eb2f83e784c5479a00edb9d35d',
+    'text/FormatFactory.class'                            : 'b8b2a7245d750929eebf82c59a34af4a',
+    'text/StrBuilder$StrBuilderReader.class'              : 'b67a32739b45226ae25297150f4a3672',
+    'text/StrBuilder$StrBuilderTokenizer.class'           : '22d8351ad6688698c281e980b5e2aef0',
+    'text/StrBuilder$StrBuilderWriter.class'              : '911b13b209c459ab0e649fcf16da119f',
+    'text/StrBuilder.class'                               : '0daf9125d2bb85d7be8f0930d65a8881',
+    'text/StrLookup$MapStrLookup.class'                   : '9468c0e1dbf42c9842f57662ab65a295',
+    'text/StrLookup.class'                                : '5755445f8e6509993c501450b4ca0283',
+    'text/StrMatcher$CharMatcher.class'                   : 'c40179bba463256255193200fad920b1',
+    'text/StrMatcher$CharSetMatcher.class'                : '93b22e1f09237f052c32ec202d95dd16',
+    'text/StrMatcher$NoMatcher.class'                     : '1bf4cce48a7fc1abd12ce0ddc32a0f21',
+    'text/StrMatcher$StringMatcher.class'                 : 'e223633d564e39846dd74ce6aa343a1d',
+    'text/StrMatcher$TrimMatcher.class'                   : '681a4bd78f7f2c5fd74a99370ad4b20b',
+    'text/StrMatcher.class'                               : 'f5a6d712ffd750ca1c7613b394ea3f22',
+    'text/StrSubstitutor.class'                           : '1cfa5ab4e0990595dec646037c5967a9',
+    'text/StrTokenizer.class'                             : 'bf73deb873f50e8ba34de3998e2972c4',
+    'time/DateFormatUtils.class'                          : 'fb537b3f1a510ac0c52804e4aff27763',
+    'time/DateUtils$DateIterator.class'                   : 'ba97d5b0027fe3e5a591f07864e44bbb',
+    'time/DateUtils.class'                                : 'df75657605e945dbbcbe886704470137',
+    'time/DurationFormatUtils$Token.class'                : 'ed64a1f1a2a41e56ae50309a1fde175c',
+    'time/DurationFormatUtils.class'                      : 'e393be23cc1ab1ff59091a1b83cc9bbd',
+    'time/FastDateFormat$CharacterLiteral.class'          : '249c346d9821e022bdb659c222dbf644',
+    'time/FastDateFormat$NumberRule.class'                : 'c41df9bc3bdff9cb4d68dd74885616b6',
+    'time/FastDateFormat$PaddedNumberField.class'         : '4cd9e98b2afc00b17d0d2038b397de24',
+    'time/FastDateFormat$Pair.class'                      : '8a934ec5d93793facff76c7e8cf3280e',
+    'time/FastDateFormat$Rule.class'                      : '442ea43c86a34ef4a5d416d3dead1ada',
+    'time/FastDateFormat$StringLiteral.class'             : '4d61134d7b4947e972418208f946417c',
+    'time/FastDateFormat$TextField.class'                 : '6894c0815fcc85c5ceb9a22df07ab4fd',
+    'time/FastDateFormat$TimeZoneDisplayKey.class'        : '20a24c7f131f38d1e4deb74572c10f45',
+    'time/FastDateFormat$TimeZoneNameRule.class'          : '529cab8e6727bf6f67f921060d5be564',
+    'time/FastDateFormat$TimeZoneNumberRule.class'        : '0217783a5cac5dc5ab9e32c7c1b82d63',
+    'time/FastDateFormat$TwelveHourField.class'           : '17aa8c1efb59e32ab1c3309a315a2d4a',
+    'time/FastDateFormat$TwentyFourHourField.class'       : 'a1a4a9dae07bb5f8406e4b21d36fe6a3',
+    'time/FastDateFormat$TwoDigitMonthField.class'        : 'c5ace49716048e8f189fc1b24bad9a96',
+    'time/FastDateFormat$TwoDigitNumberField.class'       : '92b23f96a47805d2f35df7436e268186',
+    'time/FastDateFormat$TwoDigitYearField.class'         : '876f335aad5398564e7065a6dc049e35',
+    'time/FastDateFormat$UnpaddedMonthField.class'        : '4471d8973417c87437795a7c14f6e1d9',
+    'time/FastDateFormat$UnpaddedNumberField.class'       : '26bc03b8208bb6e1f5d815029165fde9',
+    'time/FastDateFormat.class'                           : '81458555c631fd1cd7d1c7b580a252d0',
+    'time/StopWatch.class'                                : 'c9cd6ccf4eb13950ce32bccc4229d3d2',
+    'UnhandledException.class'                            : '01110b3515eb3f010cbc0485f1cb9b02',
+    'Validate.class'                                      : 'ee955facbc21ae85ba81f2ca10c2dbb9',
+    'WordUtils.class'                                     : '771ffae0f501e8378cda1a516bda46b2',
+]
+
+def jarFile = new JarFile( new File( basedir, "target/mshade-391-1.0.jar" ) )
+try
+{
+    originalChecksumsMD5.each { checksumEntry ->
+        def entryPath = "org/apache/commons/lang/$checksumEntry.key"
+        def bytes = jarFile.getInputStream(jarFile.getJarEntry(entryPath)).bytes
+        def jarEntryMD5 = MessageDigest.getInstance("MD5").digest(bytes).encodeHex().toString()
+        assert checksumEntry.value == jarEntryMD5
+    }
+    true
+}
+finally
+{
+    jarFile.close()
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
index 9226e3b..9955db0 100644
--- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
+++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
@@ -20,6 +20,7 @@
  */
 
 import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -110,7 +111,7 @@
             }
         }
 
-        RelocatorRemapper remapper = new RelocatorRemapper( shadeRequest.getRelocators() );
+        final DefaultPackageMapper packageMapper = new DefaultPackageMapper( shadeRequest.getRelocators() );
 
         // noinspection ResultOfMethodCallIgnored
         shadeRequest.getUberJar().getParentFile().mkdirs();
@@ -124,7 +125,7 @@
             MultiValuedMap<String, File> duplicates = new HashSetValuedHashMap<>( 10000, 3 );
             // CHECKSTYLE_ON: MagicNumber
 
-            shadeJars( shadeRequest, resources, transformers, remapper, out, duplicates );
+            shadeJars( shadeRequest, resources, transformers, out, duplicates, packageMapper );
 
             // CHECKSTYLE_OFF: MagicNumber
             MultiValuedMap<Collection<File>, String> overlapping = new HashSetValuedHashMap<>( 20, 15 );
@@ -222,8 +223,9 @@
     }
 
     private void shadeJars( ShadeRequest shadeRequest, Set<String> resources, List<ResourceTransformer> transformers,
-                            RelocatorRemapper remapper, JarOutputStream jos, MultiValuedMap<String, File> duplicates )
-        throws IOException, MojoExecutionException
+                            JarOutputStream jos, MultiValuedMap<String, File> duplicates,
+                            DefaultPackageMapper packageMapper )
+        throws IOException
     {
         for ( File jar : shadeRequest.getJars() )
         {
@@ -264,7 +266,7 @@
 
                     try
                     {
-                        shadeSingleJar( shadeRequest, resources, transformers, remapper, jos, duplicates, jar,
+                        shadeJarEntry( shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar,
                                         jarFile, entry, name );
                     }
                     catch ( Exception e )
@@ -278,15 +280,15 @@
         }
     }
 
-    private void shadeSingleJar( ShadeRequest shadeRequest, Set<String> resources,
-                                 List<ResourceTransformer> transformers, RelocatorRemapper remapper,
+    private void shadeJarEntry( ShadeRequest shadeRequest, Set<String> resources,
+                                 List<ResourceTransformer> transformers, DefaultPackageMapper packageMapper,
                                  JarOutputStream jos, MultiValuedMap<String, File> duplicates, File jar,
                                  JarFile jarFile, JarEntry entry, String name )
         throws IOException, MojoExecutionException
     {
         try ( InputStream in = jarFile.getInputStream( entry ) )
         {
-            String mappedName = remapper.map( name );
+            String mappedName = packageMapper.map( name, true, false );
 
             int idx = mappedName.lastIndexOf( '/' );
             if ( idx != -1 )
@@ -302,7 +304,7 @@
             duplicates.put( name, jar );
             if ( name.endsWith( ".class" ) )
             {
-                addRemappedClass( remapper, jos, jar, name, entry.getTime(), in );
+                addRemappedClass( jos, jar, name, entry.getTime(), in, packageMapper );
             }
             else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) )
             {
@@ -512,11 +514,11 @@
         resources.add( name );
     }
 
-    private void addRemappedClass( RelocatorRemapper remapper, JarOutputStream jos, File jar, String name,
-                                   long time, InputStream is )
+    private void addRemappedClass( JarOutputStream jos, File jar, String name,
+                                   long time, InputStream is, DefaultPackageMapper packageMapper )
         throws IOException, MojoExecutionException
     {
-        if ( !remapper.hasRelocators() )
+        if ( packageMapper.relocators.isEmpty() )
         {
             try
             {
@@ -532,8 +534,13 @@
 
             return;
         }
-
-        ClassReader cr = new ClassReader( is );
+        
+        // Keep the original class in, in case nothing was relocated by RelocatorRemapper. This avoids binary
+        // differences between classes, simply because they were rewritten and only details like constant pool or
+        // stack map frames are slightly different.
+        byte[] originalClass = IOUtil.toByteArray( is );
+        
+        ClassReader cr = new ClassReader( new ByteArrayInputStream( originalClass ) );
 
         // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
         // Copying the original constant pool should be avoided because it would keep references
@@ -543,24 +550,7 @@
         ClassWriter cw = new ClassWriter( 0 );
 
         final String pkg = name.substring( 0, name.lastIndexOf( '/' ) + 1 );
-        ClassVisitor cv = new ClassRemapper( cw, remapper )
-        {
-            @Override
-            public void visitSource( final String source, final String debug )
-            {
-                if ( source == null )
-                {
-                    super.visitSource( source, debug );
-                }
-                else
-                {
-                    final String fqSource = pkg + source;
-                    final String mappedSource = remapper.map( fqSource );
-                    final String filename = mappedSource.substring( mappedSource.lastIndexOf( '/' ) + 1 );
-                    super.visitSource( filename, debug );
-                }
-            }
-        };
+        final ShadeClassRemapper cv = new ShadeClassRemapper( cw, pkg, packageMapper );
 
         try
         {
@@ -571,10 +561,21 @@
             throw new MojoExecutionException( "Error in ASM processing class " + name, ise );
         }
 
-        byte[] renamedClass = cw.toByteArray();
+        // If nothing was relocated by RelocatorRemapper, write the original class, otherwise the transformed one
+        final byte[] renamedClass;
+        if ( cv.remapped )
+        {
+            logger.debug( "Rewrote class bytecode: " + name );
+            renamedClass = cw.toByteArray();
+        }
+        else
+        {
+            logger.debug( "Keeping original class bytecode: " + name );
+            renamedClass = originalClass;
+        }
 
         // Need to take the .class off for remapping evaluation
-        String mappedName = remapper.map( name.substring( 0, name.indexOf( '.' ) ) );
+        String mappedName = packageMapper.map( name.substring( 0, name.indexOf( '.' ) ), true, false );
 
         try
         {
@@ -686,89 +687,144 @@
         }
     }
 
-    static class RelocatorRemapper
-        extends Remapper
+    private interface PackageMapper
     {
+        /**
+         * Map an entity name according to the mapping rules known to this package mapper
+         * 
+         * @param entityName entity name to be mapped
+         * @param mapPaths map "slashy" names like paths or internal Java class names, e.g. {@code com/acme/Foo}?
+         * @param mapPackages  map "dotty" names like qualified Java class or package names, e.g. {@code com.acme.Foo}?
+         * @return mapped entity name, e.g. {@code org/apache/acme/Foo} or {@code org.apache.acme.Foo}
+         */
+        String map( String entityName, boolean mapPaths, boolean mapPackages );
+    }
 
-        private final Pattern classPattern = Pattern.compile( "(\\[*)?L(.+);" );
+    /**
+     * A package mapper based on a list of {@link Relocator}s
+     */
+    private static class DefaultPackageMapper implements PackageMapper
+    {
+        private static final Pattern CLASS_PATTERN = Pattern.compile( "(\\[*)?L(.+);" );
 
-        List<Relocator> relocators;
+        private final List<Relocator> relocators;
 
-        RelocatorRemapper( List<Relocator> relocators )
+        private DefaultPackageMapper( final List<Relocator> relocators )
         {
             this.relocators = relocators;
         }
 
-        public boolean hasRelocators()
+        @Override
+        public String map( String entityName, boolean mapPaths, final boolean mapPackages )
         {
-            return !relocators.isEmpty();
-        }
-
-        public Object mapValue( Object object )
-        {
-            if ( object instanceof String )
-            {
-                String name = (String) object;
-                String value = name;
-
-                String prefix = "";
-                String suffix = "";
-
-                Matcher m = classPattern.matcher( name );
-                if ( m.matches() )
-                {
-                    prefix = m.group( 1 ) + "L";
-                    suffix = ";";
-                    name = m.group( 2 );
-                }
-
-                for ( Relocator r : relocators )
-                {
-                    if ( r.canRelocateClass( name ) )
-                    {
-                        value = prefix + r.relocateClass( name ) + suffix;
-                        break;
-                    }
-                    else if ( r.canRelocatePath( name ) )
-                    {
-                        value = prefix + r.relocatePath( name ) + suffix;
-                        break;
-                    }
-                }
-
-                return value;
-            }
-
-            return super.mapValue( object );
-        }
-
-        public String map( String name )
-        {
-            String value = name;
+            String value = entityName;
 
             String prefix = "";
             String suffix = "";
 
-            Matcher m = classPattern.matcher( name );
+            Matcher m = CLASS_PATTERN.matcher( entityName );
             if ( m.matches() )
             {
                 prefix = m.group( 1 ) + "L";
                 suffix = ";";
-                name = m.group( 2 );
+                entityName = m.group( 2 );
             }
 
             for ( Relocator r : relocators )
             {
-                if ( r.canRelocatePath( name ) )
+                if ( mapPackages && r.canRelocateClass( entityName ) )
                 {
-                    value = prefix + r.relocatePath( name ) + suffix;
+                    value = prefix + r.relocateClass( entityName ) + suffix;
+                    break;
+                }
+                else if ( mapPaths && r.canRelocatePath( entityName ) )
+                {
+                    value = prefix + r.relocatePath( entityName ) + suffix;
                     break;
                 }
             }
-
             return value;
         }
-
     }
 
+    private static class LazyInitRemapper extends Remapper
+    {
+        private PackageMapper relocators;
+
+        @Override
+        public Object mapValue( Object object )
+        {
+            return object instanceof String
+                    ? relocators.map( (String) object, true, true )
+                    : super.mapValue( object );
+        }
+
+        @Override
+        public String map( String name )
+        {
+            // NOTE: Before the factoring out duplicate code from 'private String map(String, boolean)', this method did
+            // the same as 'mapValue', except for not trying to replace "dotty" package-like patterns (only "slashy"
+            // path-like ones). The refactoring retains this difference. But actually, all unit and integration tests
+            // still pass, if both variants are unified into one which always tries to replace both pattern types.
+            //
+            //  TODO: Analyse if this case is really necessary and has any special meaning or avoids any known problems.
+            //   If not, then simplify DefaultShader.PackageMapper.map to only have the String parameter and assume
+            //   both boolean ones to always be true.
+            return relocators.map( name, true, false );
+        }
+    }
+
+    // TODO: we can avoid LazyInitRemapper N instantiations (and use a singleton)
+    //       reimplementing ClassRemapper there.
+    //       It looks a bad idea but actually enables us to respect our relocation API which has no
+    //       consistency with ASM one which can lead to multiple issues for short relocation patterns
+    //       plus overcome ClassRemapper limitations we can care about (see its javadoc for details).
+    //
+    // NOTE: very short term we can just reuse the same LazyInitRemapper and let the constructor set it.
+    //       since multithreading is not faster in this processing it would be more than sufficient if
+    //       caring of this 2 objects per class allocation (but keep in mind the visitor will allocate way more ;)).
+    //       Last point which makes it done this way as of now is that perf seems not impacted at all.
+    private static class ShadeClassRemapper extends ClassRemapper implements PackageMapper
+    {
+        private final String pkg;
+        private final PackageMapper packageMapper;
+        private boolean remapped;
+
+        ShadeClassRemapper( final ClassVisitor classVisitor, final String pkg,
+                            final DefaultPackageMapper packageMapper )
+        {
+            super( classVisitor, new LazyInitRemapper() /* can't be init in the constructor with "this" */ );
+            this.pkg = pkg;
+            this.packageMapper = packageMapper;
+
+            // use this to enrich relocators impl with "remapped" logic
+            LazyInitRemapper.class.cast( remapper ).relocators = this;
+        }
+
+        @Override
+        public void visitSource( final String source, final String debug )
+        {
+            if ( source == null )
+            {
+                super.visitSource( null, debug );
+                return;
+            }
+
+            final String fqSource = pkg + source;
+            final String mappedSource = map( fqSource, true, false );
+            final String filename = mappedSource.substring( mappedSource.lastIndexOf( '/' ) + 1 );
+            super.visitSource( filename, debug );
+        }
+
+        @Override
+        public String map( final String entityName, boolean mapPaths, final boolean mapPackages )
+        {
+            final String mapped = packageMapper.map( entityName, true, mapPackages );
+            if ( !remapped )
+            {
+                remapped = !mapped.equals( entityName );
+            }
+            return mapped;
+        }
+    }
 }
diff --git a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
index d48fd43..a709018 100644
--- a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
+++ b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
@@ -23,6 +23,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -57,12 +58,15 @@
 import org.objectweb.asm.Opcodes;
 import org.slf4j.Logger;
 
+import static java.util.Objects.requireNonNull;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.hasItems;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -77,6 +81,62 @@
         "org/codehaus/plexus/util/xml/pull.*" };
 
     @Test
+    public void testNoopWhenNotRelocated() throws IOException, MojoExecutionException {
+        final File plexusJar = new File("src/test/jars/plexus-utils-1.4.1.jar" );
+        final File shadedOutput = new File( "target/foo-custom_testNoopWhenNotRelocated.jar" );
+
+        final Set<File> jars = new LinkedHashSet<>();
+        jars.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
+        jars.add( plexusJar );
+
+        final Relocator relocator = new SimpleRelocator(
+            "org/codehaus/plexus/util/cli", "relocated/plexus/util/cli",
+            Collections.<String>emptyList(), Collections.<String>emptyList() );
+
+        final ShadeRequest shadeRequest = new ShadeRequest();
+        shadeRequest.setJars( jars );
+        shadeRequest.setRelocators( Collections.singletonList( relocator ) );
+        shadeRequest.setResourceTransformers( Collections.<ResourceTransformer>emptyList() );
+        shadeRequest.setFilters( Collections.<Filter>emptyList() );
+        shadeRequest.setUberJar( shadedOutput );
+
+        final DefaultShader shader = newShader();
+        shader.shade( shadeRequest );
+
+        try ( final JarFile originalJar = new JarFile( plexusJar );
+              final JarFile shadedJar = new JarFile( shadedOutput ) )
+        {
+            // ASM processes all class files. In doing so, it modifies them, even when not relocating anything.
+            // Before MSHADE-391, the processed files were written to the uber JAR, which did no harm, but made it
+            // difficult to find out by simple file comparison, if a file was actually relocated or not. Now, Shade
+            // makes sure to always write the original file if the class neither was relocated itself nor references
+            // other, relocated classes. So we are checking for regressions here. 
+            assertTrue( areEqual( originalJar, shadedJar,
+                "org/codehaus/plexus/util/Expand.class" ) );
+
+            // Relocated files should always be different, because they contain different package names in their byte
+            // code. We should verify this anyway, in order to avoid an existing class file from simply being moved to
+            // another location without actually having been relocated internally.
+            assertFalse( areEqual(
+                originalJar, shadedJar,
+                "org/codehaus/plexus/util/cli/Arg.class", "relocated/plexus/util/cli/Arg.class" ) );
+        }
+        int result = 0;
+        for ( final String msg : debugMessages.getAllValues() )
+        {
+            if ( "Rewrote class bytecode: org/codehaus/plexus/util/cli/Arg.class".equals(msg) )
+            {
+                result |= 1;
+            }
+            else if ( "Keeping original class bytecode: org/codehaus/plexus/util/Expand.class".equals(msg) )
+            {
+                result |= 2;
+            }
+        }
+        assertEquals( 3 /* 1 | 2 */ , result);
+    }
+
+    @Test
     public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException {
         final DefaultShader shader = newShader();
 
@@ -388,4 +448,21 @@
         doNothing().when(logger).warn(warnMessages.capture());
         return logger;
     }
+
+    private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry ) throws IOException
+    {
+        return areEqual( jar1, jar2, entry, entry );
+    }
+
+    private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry1, String entry2 )
+        throws IOException
+    {
+        try ( final InputStream s1 = jar1.getInputStream(
+                requireNonNull(jar1.getJarEntry(entry1), entry1 + " in " + jar1.getName() ) );
+              final InputStream s2 = jar2.getInputStream(
+                      requireNonNull(jar2.getJarEntry(entry2), entry2 + " in " + jar2.getName() ) ))
+        {
+            return Arrays.equals( IOUtil.toByteArray( s1 ), IOUtil.toByteArray( s2 ) );
+        }
+    }
 }