• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // ASM: a very small and fast Java bytecode manipulation framework
2 // Copyright (c) 2000-2011 INRIA, France Telecom
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 // 3. Neither the name of the copyright holders nor the names of its
14 //    contributors may be used to endorse or promote products derived from
15 //    this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
27 // THE POSSIBILITY OF SUCH DAMAGE.
28 package org.objectweb.asm.commons;
29 
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import org.objectweb.asm.ConstantDynamic;
35 import org.objectweb.asm.Handle;
36 import org.objectweb.asm.Label;
37 import org.objectweb.asm.MethodVisitor;
38 import org.objectweb.asm.Opcodes;
39 import org.objectweb.asm.Type;
40 
41 /**
42  * A {@link MethodVisitor} to insert before, after and around advices in methods and constructors.
43  * For constructors, the code keeps track of the elements on the stack in order to detect when the
44  * super class constructor is called (note that there can be multiple such calls in different
45  * branches). {@code onMethodEnter} is called after each super class constructor call, because the
46  * object cannot be used before it is properly initialized.
47  *
48  * @author Eugene Kuleshov
49  * @author Eric Bruneton
50  */
51 public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
52 
53   /** The "uninitialized this" value. */
54   private static final Object UNINITIALIZED_THIS = new Object();
55 
56   /** Any value other than "uninitialized this". */
57   private static final Object OTHER = new Object();
58 
59   /** Prefix of the error message when invalid opcodes are found. */
60   private static final String INVALID_OPCODE = "Invalid opcode ";
61 
62   /** The access flags of the visited method. */
63   protected int methodAccess;
64 
65   /** The descriptor of the visited method. */
66   protected String methodDesc;
67 
68   /** Whether the visited method is a constructor. */
69   private final boolean isConstructor;
70 
71   /**
72    * Whether the super class constructor has been called (if the visited method is a constructor),
73    * at the current instruction. There can be multiple call sites to the super constructor (e.g. for
74    * Java code such as {@code super(expr ? value1 : value2);}), in different branches. When scanning
75    * the bytecode linearly, we can move from one branch where the super constructor has been called
76    * to another where it has not been called yet. Therefore, this value can change from false to
77    * true, and vice-versa.
78    */
79   private boolean superClassConstructorCalled;
80 
81   /**
82    * The values on the current execution stack frame (long and double are represented by two
83    * elements). Each value is either {@link #UNINITIALIZED_THIS} (for the uninitialized this value),
84    * or {@link #OTHER} (for any other value). This field is only maintained for constructors, in
85    * branches where the super class constructor has not been called yet.
86    */
87   private List<Object> stackFrame;
88 
89   /**
90    * The stack map frames corresponding to the labels of the forward jumps made *before* the super
91    * class constructor has been called (note that the Java Virtual Machine forbids backward jumps
92    * before the super class constructor is called). Note that by definition (cf. the 'before'), when
93    * we reach a label from this map, {@link #superClassConstructorCalled} must be reset to false.
94    * This field is only maintained for constructors.
95    */
96   private Map<Label, List<Object>> forwardJumpStackFrames;
97 
98   /**
99    * Constructs a new {@link AdviceAdapter}.
100    *
101    * @param api the ASM API version implemented by this visitor. Must be one of the {@code
102    *     ASM}<i>x</i> values in {@link Opcodes}.
103    * @param methodVisitor the method visitor to which this adapter delegates calls.
104    * @param access the method's access flags (see {@link Opcodes}).
105    * @param name the method's name.
106    * @param descriptor the method's descriptor (see {@link Type Type}).
107    */
AdviceAdapter( final int api, final MethodVisitor methodVisitor, final int access, final String name, final String descriptor)108   protected AdviceAdapter(
109       final int api,
110       final MethodVisitor methodVisitor,
111       final int access,
112       final String name,
113       final String descriptor) {
114     super(api, methodVisitor, access, name, descriptor);
115     methodAccess = access;
116     methodDesc = descriptor;
117     isConstructor = "<init>".equals(name);
118   }
119 
120   @Override
visitCode()121   public void visitCode() {
122     super.visitCode();
123     if (isConstructor) {
124       stackFrame = new ArrayList<>();
125       forwardJumpStackFrames = new HashMap<>();
126     } else {
127       onMethodEnter();
128     }
129   }
130 
131   @Override
visitLabel(final Label label)132   public void visitLabel(final Label label) {
133     super.visitLabel(label);
134     if (isConstructor && forwardJumpStackFrames != null) {
135       List<Object> labelStackFrame = forwardJumpStackFrames.get(label);
136       if (labelStackFrame != null) {
137         stackFrame = labelStackFrame;
138         superClassConstructorCalled = false;
139         forwardJumpStackFrames.remove(label);
140       }
141     }
142   }
143 
144   @Override
visitInsn(final int opcode)145   public void visitInsn(final int opcode) {
146     if (isConstructor && !superClassConstructorCalled) {
147       int stackSize;
148       switch (opcode) {
149         case IRETURN:
150         case FRETURN:
151         case ARETURN:
152         case LRETURN:
153         case DRETURN:
154           throw new IllegalArgumentException("Invalid return in constructor");
155         case RETURN: // empty stack
156           onMethodExit(opcode);
157           endConstructorBasicBlockWithoutSuccessor();
158           break;
159         case ATHROW: // 1 before n/a after
160           popValue();
161           onMethodExit(opcode);
162           endConstructorBasicBlockWithoutSuccessor();
163           break;
164         case NOP:
165         case LALOAD: // remove 2 add 2
166         case DALOAD: // remove 2 add 2
167         case LNEG:
168         case DNEG:
169         case FNEG:
170         case INEG:
171         case L2D:
172         case D2L:
173         case F2I:
174         case I2B:
175         case I2C:
176         case I2S:
177         case I2F:
178         case ARRAYLENGTH:
179           break;
180         case ACONST_NULL:
181         case ICONST_M1:
182         case ICONST_0:
183         case ICONST_1:
184         case ICONST_2:
185         case ICONST_3:
186         case ICONST_4:
187         case ICONST_5:
188         case FCONST_0:
189         case FCONST_1:
190         case FCONST_2:
191         case F2L: // 1 before 2 after
192         case F2D:
193         case I2L:
194         case I2D:
195           pushValue(OTHER);
196           break;
197         case LCONST_0:
198         case LCONST_1:
199         case DCONST_0:
200         case DCONST_1:
201           pushValue(OTHER);
202           pushValue(OTHER);
203           break;
204         case IALOAD: // remove 2 add 1
205         case FALOAD: // remove 2 add 1
206         case AALOAD: // remove 2 add 1
207         case BALOAD: // remove 2 add 1
208         case CALOAD: // remove 2 add 1
209         case SALOAD: // remove 2 add 1
210         case POP:
211         case IADD:
212         case FADD:
213         case ISUB:
214         case LSHL: // 3 before 2 after
215         case LSHR: // 3 before 2 after
216         case LUSHR: // 3 before 2 after
217         case L2I: // 2 before 1 after
218         case L2F: // 2 before 1 after
219         case D2I: // 2 before 1 after
220         case D2F: // 2 before 1 after
221         case FSUB:
222         case FMUL:
223         case FDIV:
224         case FREM:
225         case FCMPL: // 2 before 1 after
226         case FCMPG: // 2 before 1 after
227         case IMUL:
228         case IDIV:
229         case IREM:
230         case ISHL:
231         case ISHR:
232         case IUSHR:
233         case IAND:
234         case IOR:
235         case IXOR:
236         case MONITORENTER:
237         case MONITOREXIT:
238           popValue();
239           break;
240         case POP2:
241         case LSUB:
242         case LMUL:
243         case LDIV:
244         case LREM:
245         case LADD:
246         case LAND:
247         case LOR:
248         case LXOR:
249         case DADD:
250         case DMUL:
251         case DSUB:
252         case DDIV:
253         case DREM:
254           popValue();
255           popValue();
256           break;
257         case IASTORE:
258         case FASTORE:
259         case AASTORE:
260         case BASTORE:
261         case CASTORE:
262         case SASTORE:
263         case LCMP: // 4 before 1 after
264         case DCMPL:
265         case DCMPG:
266           popValue();
267           popValue();
268           popValue();
269           break;
270         case LASTORE:
271         case DASTORE:
272           popValue();
273           popValue();
274           popValue();
275           popValue();
276           break;
277         case DUP:
278           pushValue(peekValue());
279           break;
280         case DUP_X1:
281           stackSize = stackFrame.size();
282           stackFrame.add(stackSize - 2, stackFrame.get(stackSize - 1));
283           break;
284         case DUP_X2:
285           stackSize = stackFrame.size();
286           stackFrame.add(stackSize - 3, stackFrame.get(stackSize - 1));
287           break;
288         case DUP2:
289           stackSize = stackFrame.size();
290           stackFrame.add(stackSize - 2, stackFrame.get(stackSize - 1));
291           stackFrame.add(stackSize - 2, stackFrame.get(stackSize - 1));
292           break;
293         case DUP2_X1:
294           stackSize = stackFrame.size();
295           stackFrame.add(stackSize - 3, stackFrame.get(stackSize - 1));
296           stackFrame.add(stackSize - 3, stackFrame.get(stackSize - 1));
297           break;
298         case DUP2_X2:
299           stackSize = stackFrame.size();
300           stackFrame.add(stackSize - 4, stackFrame.get(stackSize - 1));
301           stackFrame.add(stackSize - 4, stackFrame.get(stackSize - 1));
302           break;
303         case SWAP:
304           stackSize = stackFrame.size();
305           stackFrame.add(stackSize - 2, stackFrame.get(stackSize - 1));
306           stackFrame.remove(stackSize);
307           break;
308         default:
309           throw new IllegalArgumentException(INVALID_OPCODE + opcode);
310       }
311     } else {
312       switch (opcode) {
313         case RETURN:
314         case IRETURN:
315         case FRETURN:
316         case ARETURN:
317         case LRETURN:
318         case DRETURN:
319         case ATHROW:
320           onMethodExit(opcode);
321           break;
322         default:
323           break;
324       }
325     }
326     super.visitInsn(opcode);
327   }
328 
329   @Override
visitVarInsn(final int opcode, final int varIndex)330   public void visitVarInsn(final int opcode, final int varIndex) {
331     super.visitVarInsn(opcode, varIndex);
332     if (isConstructor && !superClassConstructorCalled) {
333       switch (opcode) {
334         case ILOAD:
335         case FLOAD:
336           pushValue(OTHER);
337           break;
338         case LLOAD:
339         case DLOAD:
340           pushValue(OTHER);
341           pushValue(OTHER);
342           break;
343         case ALOAD:
344           pushValue(varIndex == 0 ? UNINITIALIZED_THIS : OTHER);
345           break;
346         case ASTORE:
347         case ISTORE:
348         case FSTORE:
349           popValue();
350           break;
351         case LSTORE:
352         case DSTORE:
353           popValue();
354           popValue();
355           break;
356         case RET:
357           endConstructorBasicBlockWithoutSuccessor();
358           break;
359         default:
360           throw new IllegalArgumentException(INVALID_OPCODE + opcode);
361       }
362     }
363   }
364 
365   @Override
visitFieldInsn( final int opcode, final String owner, final String name, final String descriptor)366   public void visitFieldInsn(
367       final int opcode, final String owner, final String name, final String descriptor) {
368     super.visitFieldInsn(opcode, owner, name, descriptor);
369     if (isConstructor && !superClassConstructorCalled) {
370       char firstDescriptorChar = descriptor.charAt(0);
371       boolean longOrDouble = firstDescriptorChar == 'J' || firstDescriptorChar == 'D';
372       switch (opcode) {
373         case GETSTATIC:
374           pushValue(OTHER);
375           if (longOrDouble) {
376             pushValue(OTHER);
377           }
378           break;
379         case PUTSTATIC:
380           popValue();
381           if (longOrDouble) {
382             popValue();
383           }
384           break;
385         case PUTFIELD:
386           popValue();
387           popValue();
388           if (longOrDouble) {
389             popValue();
390           }
391           break;
392         case GETFIELD:
393           if (longOrDouble) {
394             pushValue(OTHER);
395           }
396           break;
397         default:
398           throw new IllegalArgumentException(INVALID_OPCODE + opcode);
399       }
400     }
401   }
402 
403   @Override
visitIntInsn(final int opcode, final int operand)404   public void visitIntInsn(final int opcode, final int operand) {
405     super.visitIntInsn(opcode, operand);
406     if (isConstructor && !superClassConstructorCalled && opcode != NEWARRAY) {
407       pushValue(OTHER);
408     }
409   }
410 
411   @Override
visitLdcInsn(final Object value)412   public void visitLdcInsn(final Object value) {
413     super.visitLdcInsn(value);
414     if (isConstructor && !superClassConstructorCalled) {
415       pushValue(OTHER);
416       if (value instanceof Double
417           || value instanceof Long
418           || (value instanceof ConstantDynamic && ((ConstantDynamic) value).getSize() == 2)) {
419         pushValue(OTHER);
420       }
421     }
422   }
423 
424   @Override
visitMultiANewArrayInsn(final String descriptor, final int numDimensions)425   public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) {
426     super.visitMultiANewArrayInsn(descriptor, numDimensions);
427     if (isConstructor && !superClassConstructorCalled) {
428       for (int i = 0; i < numDimensions; i++) {
429         popValue();
430       }
431       pushValue(OTHER);
432     }
433   }
434 
435   @Override
visitTypeInsn(final int opcode, final String type)436   public void visitTypeInsn(final int opcode, final String type) {
437     super.visitTypeInsn(opcode, type);
438     // ANEWARRAY, CHECKCAST or INSTANCEOF don't change stack.
439     if (isConstructor && !superClassConstructorCalled && opcode == NEW) {
440       pushValue(OTHER);
441     }
442   }
443 
444   @Override
visitMethodInsn( final int opcodeAndSource, final String owner, final String name, final String descriptor, final boolean isInterface)445   public void visitMethodInsn(
446       final int opcodeAndSource,
447       final String owner,
448       final String name,
449       final String descriptor,
450       final boolean isInterface) {
451     if (api < Opcodes.ASM5 && (opcodeAndSource & Opcodes.SOURCE_DEPRECATED) == 0) {
452       // Redirect the call to the deprecated version of this method.
453       super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
454       return;
455     }
456     super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
457     int opcode = opcodeAndSource & ~Opcodes.SOURCE_MASK;
458 
459     doVisitMethodInsn(opcode, name, descriptor);
460   }
461 
doVisitMethodInsn(final int opcode, final String name, final String descriptor)462   private void doVisitMethodInsn(final int opcode, final String name, final String descriptor) {
463     if (isConstructor && !superClassConstructorCalled) {
464       for (Type argumentType : Type.getArgumentTypes(descriptor)) {
465         popValue();
466         if (argumentType.getSize() == 2) {
467           popValue();
468         }
469       }
470       switch (opcode) {
471         case INVOKEINTERFACE:
472         case INVOKEVIRTUAL:
473           popValue();
474           break;
475         case INVOKESPECIAL:
476           Object value = popValue();
477           if (value == UNINITIALIZED_THIS
478               && !superClassConstructorCalled
479               && name.equals("<init>")) {
480             superClassConstructorCalled = true;
481             onMethodEnter();
482           }
483           break;
484         default:
485           break;
486       }
487 
488       Type returnType = Type.getReturnType(descriptor);
489       if (returnType != Type.VOID_TYPE) {
490         pushValue(OTHER);
491         if (returnType.getSize() == 2) {
492           pushValue(OTHER);
493         }
494       }
495     }
496   }
497 
498   @Override
visitInvokeDynamicInsn( final String name, final String descriptor, final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments)499   public void visitInvokeDynamicInsn(
500       final String name,
501       final String descriptor,
502       final Handle bootstrapMethodHandle,
503       final Object... bootstrapMethodArguments) {
504     super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
505     doVisitMethodInsn(Opcodes.INVOKEDYNAMIC, name, descriptor);
506   }
507 
508   @Override
visitJumpInsn(final int opcode, final Label label)509   public void visitJumpInsn(final int opcode, final Label label) {
510     super.visitJumpInsn(opcode, label);
511     if (isConstructor && !superClassConstructorCalled) {
512       switch (opcode) {
513         case IFEQ:
514         case IFNE:
515         case IFLT:
516         case IFGE:
517         case IFGT:
518         case IFLE:
519         case IFNULL:
520         case IFNONNULL:
521           popValue();
522           break;
523         case IF_ICMPEQ:
524         case IF_ICMPNE:
525         case IF_ICMPLT:
526         case IF_ICMPGE:
527         case IF_ICMPGT:
528         case IF_ICMPLE:
529         case IF_ACMPEQ:
530         case IF_ACMPNE:
531           popValue();
532           popValue();
533           break;
534         case JSR:
535           pushValue(OTHER);
536           break;
537         case GOTO:
538           endConstructorBasicBlockWithoutSuccessor();
539           break;
540         default:
541           break;
542       }
543       addForwardJump(label);
544     }
545   }
546 
547   @Override
visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels)548   public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
549     super.visitLookupSwitchInsn(dflt, keys, labels);
550     if (isConstructor && !superClassConstructorCalled) {
551       popValue();
552       addForwardJumps(dflt, labels);
553       endConstructorBasicBlockWithoutSuccessor();
554     }
555   }
556 
557   @Override
visitTableSwitchInsn( final int min, final int max, final Label dflt, final Label... labels)558   public void visitTableSwitchInsn(
559       final int min, final int max, final Label dflt, final Label... labels) {
560     super.visitTableSwitchInsn(min, max, dflt, labels);
561     if (isConstructor && !superClassConstructorCalled) {
562       popValue();
563       addForwardJumps(dflt, labels);
564       endConstructorBasicBlockWithoutSuccessor();
565     }
566   }
567 
568   @Override
visitTryCatchBlock( final Label start, final Label end, final Label handler, final String type)569   public void visitTryCatchBlock(
570       final Label start, final Label end, final Label handler, final String type) {
571     super.visitTryCatchBlock(start, end, handler, type);
572     // By definition of 'forwardJumpStackFrames', 'handler' should be pushed only if there is an
573     // instruction between 'start' and 'end' at which the super class constructor is not yet
574     // called. Unfortunately, try catch blocks must be visited before their labels, so we have no
575     // way to know this at this point. Instead, we suppose that the super class constructor has not
576     // been called at the start of *any* exception handler. If this is wrong, normally there should
577     // not be a second super class constructor call in the exception handler (an object can't be
578     // initialized twice), so this is not issue (in the sense that there is no risk to emit a wrong
579     // 'onMethodEnter').
580     if (isConstructor && !forwardJumpStackFrames.containsKey(handler)) {
581       List<Object> handlerStackFrame = new ArrayList<>();
582       handlerStackFrame.add(OTHER);
583       forwardJumpStackFrames.put(handler, handlerStackFrame);
584     }
585   }
586 
addForwardJumps(final Label dflt, final Label[] labels)587   private void addForwardJumps(final Label dflt, final Label[] labels) {
588     addForwardJump(dflt);
589     for (Label label : labels) {
590       addForwardJump(label);
591     }
592   }
593 
addForwardJump(final Label label)594   private void addForwardJump(final Label label) {
595     if (forwardJumpStackFrames.containsKey(label)) {
596       return;
597     }
598     forwardJumpStackFrames.put(label, new ArrayList<>(stackFrame));
599   }
600 
endConstructorBasicBlockWithoutSuccessor()601   private void endConstructorBasicBlockWithoutSuccessor() {
602     // The next instruction is not reachable from this instruction. If it is dead code, we
603     // should not try to simulate stack operations, and there is no need to insert advices
604     // here. If it is reachable with a backward jump, the only possible case is that the super
605     // class constructor has already been called (backward jumps are forbidden before it is
606     // called). If it is reachable with a forward jump, there are two sub-cases. Either the
607     // super class constructor has already been called when reaching the next instruction, or
608     // it has not been called. But in this case there must be a forwardJumpStackFrames entry
609     // for a Label designating the next instruction, and superClassConstructorCalled will be
610     // reset to false there. We can therefore always reset this field to true here.
611     superClassConstructorCalled = true;
612   }
613 
popValue()614   private Object popValue() {
615     return stackFrame.remove(stackFrame.size() - 1);
616   }
617 
peekValue()618   private Object peekValue() {
619     return stackFrame.get(stackFrame.size() - 1);
620   }
621 
pushValue(final Object value)622   private void pushValue(final Object value) {
623     stackFrame.add(value);
624   }
625 
626   /**
627    * Generates the "before" advice for the visited method. The default implementation of this method
628    * does nothing. Subclasses can use or change all the local variables, but should not change state
629    * of the stack. This method is called at the beginning of the method or after super class
630    * constructor has been called (in constructors).
631    */
onMethodEnter()632   protected void onMethodEnter() {}
633 
634   /**
635    * Generates the "after" advice for the visited method. The default implementation of this method
636    * does nothing. Subclasses can use or change all the local variables, but should not change state
637    * of the stack. This method is called at the end of the method, just before return and athrow
638    * instructions. The top element on the stack contains the return value or the exception instance.
639    * For example:
640    *
641    * <pre>
642    * public void onMethodExit(final int opcode) {
643    *   if (opcode == RETURN) {
644    *     visitInsn(ACONST_NULL);
645    *   } else if (opcode == ARETURN || opcode == ATHROW) {
646    *     dup();
647    *   } else {
648    *     if (opcode == LRETURN || opcode == DRETURN) {
649    *       dup2();
650    *     } else {
651    *       dup();
652    *     }
653    *     box(Type.getReturnType(this.methodDesc));
654    *   }
655    *   visitIntInsn(SIPUSH, opcode);
656    *   visitMethodInsn(INVOKESTATIC, owner, "onExit", "(Ljava/lang/Object;I)V");
657    * }
658    *
659    * // An actual call back method.
660    * public static void onExit(final Object exitValue, final int opcode) {
661    *   ...
662    * }
663    * </pre>
664    *
665    * @param opcode one of {@link Opcodes#RETURN}, {@link Opcodes#IRETURN}, {@link Opcodes#FRETURN},
666    *     {@link Opcodes#ARETURN}, {@link Opcodes#LRETURN}, {@link Opcodes#DRETURN} or {@link
667    *     Opcodes#ATHROW}.
668    */
onMethodExit(final int opcode)669   protected void onMethodExit(final int opcode) {}
670 }
671